OCI OpenSearchのk-NNをlangchainで試してみる

# はじめに
この記事では、Oracle Cloud Infrastructure (OCI) のOpenSearchサービスにおけるk-NN (k近傍法) を利用したセマンティック検索の設定とデモンストレーションを行います。
セマンティック検索は、単なるキーワードマッチングを超え、文書の意味的な理解に基づいて検索結果を提供する技術です。
本記事では、その基本的な概念から実際のデモ構築までをステップバイステップで解説します。


https://blogs.oracle.com/oracle4engineer/post/ja-oci-search-with-opensearch-211-ai-innovations


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/75623393-c151-b7b8-94fb-51e2c06e4776.png)


# セマンティック検索とは
セマンティック検索は、検索語の文脈や意味を理解し、より関連性の高い検索結果を提供する技術です。このアプローチにより、単語の表層的な一致ではなく、クエリの意図や文書の内容の深い理解に基づいて情報を取得することが可能になります。
例えば、同じ単語が異なる文脈で異なる意味を持つ場合、セマンティック検索はその違いを識別し、より精度の高い結果を提供します。

今回OpenSearchを用いたセマンティック検索をするにあたってlangchainを使用します。
langchainを使用すると、OCI Cohere embeddingとの連携、検索ロジックの実装が容易に実現可能です！

デモで使用した各モジュールのバージョンは以下です：
* Python 3.12.3
* oci 2.125.2
* langchain                 0.1.16
* opensearch-py             2.5.0

# 環境構築
今回構築するOCI環境は下記です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/443c19e8-4166-a778-2671-e05d97921e78.png)

VMはOpenSearchインスタンスにログインするための踏み台、かつデモアプリ用Webサーバとして利用します。

### OpenSearchインスタンス構築
まず最初にOCIコンソールからOpenSearchインスタンスを作成します。
※前提としてVCNは構築済みとします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/e9d28c92-d417-20d4-30d1-a81453d2cc54.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/842edfa6-064f-4dd6-4389-bf37e54280e1.png)

バージョンはk-NNが使える2.11.0にします。

注意書きにもある通り、事前にポリシーを設定しておきます：
```shell:opensearchに必要なポリシー
Allow service opensearch to manage vnics in compartment <compartment_name>
Allow service opensearch to use subnets in compartment <compartment_name>
Allow service opensearch to use network-security-groups in compartment <compartment_name>
Allow service opensearch to manage vcns in compartment <compartment_name>
```

あとは
* セキュリティの構成は、任意のユーザ・パスワードを設定
* ノードの構成は「開発」を選択（全ノード1 OCPUから選択できます）
* ネットワーキングの構成は、既存のVCN、サブネットを選択します

OpenSearchインスタンスが立ち上がるまで20分ほどかかりますので気長に待ちましょう！




### VM構築
Pythonを最新化した後、下記の必要なモジュールをインストールします：
```shell:
pip install streamlit opensearch-py langchain oci
```

またstreamlitで使用する8501ポートのfirewallを開放しておきましょう。
```shell:
sudo firewall-cmd --zone=public --permanent --add-port=8501/tcp
sudo firewall-cmd --reload
```

### ベクトルインデックス作成

作成したVMからOpenSearchにアクセスしてインデックスを作成します。
今回はlangchainを使って、OCI VMの説明文書をテキストファイルに落とし込み、インデックスに新規投入します。
embedding LLMはcohereの多言語モデル(cohere.embed-multilingual-v3.0)をoci python SDKより使用します。


```python:emb.py
from langchain_community.vectorstores import OpenSearchVectorSearch
from langchain_community.embeddings import OCIGenAIEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import OpenSearchVectorSearch
from langchain_text_splitters import CharacterTextSplitter
from langchain_text_splitters import RecursiveCharacterTextSplitter

embeddings = OCIGenAIEmbeddings(
    model_id="cohere.embed-multilingual-v3.0",
    service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
    compartment_id="ocid1.compartment.oc1..aaa...",
)
loader = TextLoader(
    "./ocifaq.txt", encoding='utf-8')
    # "./ocidoc.txt", encoding='utf-8')
documents = loader.load()
# text_splitter = CharacterTextSplitter(
#     separator="\n\n",  chunk_size=500, chunk_overlap=100)
text_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n"],
    chunk_size=500,
    chunk_overlap=100,
    length_function=len,
    is_separator_regex=False,
)
docs = text_splitter.split_documents(documents)
# print(len(docs))
max_chunk_size = 95
for chunk_start in range(0, len(docs), max_chunk_size):
    chunk_docs = docs[chunk_start:chunk_start + max_chunk_size]
    docsearch = OpenSearchVectorSearch.from_documents(
        chunk_docs, embeddings, opensearch_url="https:/aaa....opensearch.us-ashburn-1.oci.oraclecloud.com:9200", http_auth=("user", "password"),
        use_ssl=False,
        verify_certs=False,
        ssl_assert_hostname=False,
        ssl_show_warn=False,
        index_name="vm_faq",
        # index_name="vm_doc",
    )
print("finish")
```

```shell:
python emb.py
```

上記embeddingを実行すると、指定したインデックスが作られます。
vm_faq, vm_docが作成したインデックスです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/e6b83c29-ff25-0f71-89f1-ce831b0b180c.png)

:::note info
OpenSearchダッシュボード、apiエンドポイントへは下記のように踏み台経由でlocalhostアクセスが可能です。
```shell:
ssh -C -v -t -L 127.0.0.1:5601:(apiプライベートIP):5601 -L 127.0.0.1:9200:(ダッシュボードプライベートIP):9200 opc@(vmのパブリックIP) -i "(認証キーのパス)"
:::

OpenSearchダッシュボードのdevtoolを使って作成されたテーブルを見てみます。

```shell:インデックスの設定確認
GET vm_faq/_mapping?pretty
{
  "query": {
    "match_all": {}
  }
}
```
```shell:取得結果
{
  "vm_faq": {
    "mappings": {
      "properties": {
        "metadata": {
            ...
        },
        "text": {
            ...
        },
        "vector_field": {
          "type": "knn_vector",
          "dimension": 1024,
          "method": {
            "engine": "nmslib",
            "space_type": "l2",
            "name": "hnsw",
            "parameters": {
              "ef_construction": 512,
              "m": 16
            }
          }
        }
      }
    }
  }
}
```
作成されたvm_faqインデックスについて、
特にvector_fielsの値を見ると、knn-vector型として定義できていることがわかります（エンジンはnmslib）



### デモアプリ構築

streamlitを用いて、先ほど作成した2インデックスを検索対象とするセマンティック検索デモアプリを作ります。



```python:app.py
import streamlit as st
from langchain.schema import Document
from langchain_community.vectorstores import OpenSearchVectorSearch
from langchain_community.embeddings import OCIGenAIEmbeddings
from langchain_community.vectorstores import OpenSearchVectorSearch
import pandas as pd

def main():
    st.title('OpenSearch Vector Search App🔍')
    with st.expander("ベクトルデータは以下の2リンクの情報を使用しています"):
        st.markdown("[Oracle Cloud Infrastructure - コンピュート概要](https://docs.oracle.com/ja-jp/iaas/Content/Compute/Concepts/computeoverview.htm)")
        st.markdown("[Oracle Cloud Compute - FAQ](https://www.oracle.com/jp/cloud/compute/faq/)")
    user_input = st.text_input('検索ワードを入力してください')
    k_value = st.selectbox(
    'k-NN (k近傍法)の値',
    range(1, 11),
    index=2  # デフォルトは3（0から数えて3番目）
)
    embeddings = OCIGenAIEmbeddings(
        model_id="cohere.embed-multilingual-v3.0",
        service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
        compartment_id="ocid1.compartment.oc1..aaa...",
    )
    docsearch = OpenSearchVectorSearch(
        index_name="vm_*", # 複数のドキュメント（インデックス）を対象にできます
        embedding_function=embeddings,
        opensearch_url="https://amaaaaa....opensearch.us-ashburn-1.oci.oraclecloud.com:9200",
        http_auth=("user", "password"),
    )
    if st.button('実行'):
        if not user_input:
            st.write('テキストが入力されていません。')
            return
        st.write(f'入力されたテキスト: {user_input}')
        docs = docsearch.similarity_search(
            user_input, k=k_value)
        data = []
        for doc in docs:
            page_content = doc.page_content.replace('\n', '<br>').replace('\t', '&#009;')
            source = doc.metadata['source']
            data.append({'page_content': page_content, 'source': source})
        df = pd.DataFrame(data)
        st.markdown(df.to_html(escape=False), unsafe_allow_html=True)

if __name__ == '__main__':
    main()

```
```shell:
streamlit run app.py
```

上記コマンド実行後、`http://(vm public ip):8501`にアクセスすることで、アプリを操作できます。
※VCNのセキュリティリストに8501ポートを疎通させる設定は忘れずに。

↓任意の検索ワードとk-nnの値を指定できるアプリが立ち上がります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/98a5dca7-5cf1-fd87-9613-2f31af8ec1d6.png)



### 動作確認

本サンプルではoci vmの下記情報をOpenSearchにそのままインデックスとして登録しています。
[Oracle Cloud Compute - FAQ](https://www.oracle.com/jp/cloud/compute/faq/)
[Oracle Cloud Infrastructure - コンピュート概要](https://docs.oracle.com/ja-jp/iaas/Content/Compute/Concepts/computeoverview.htm)

まず最初に、「フォルトドメインとは」で検索すると該当するベクトルデータのチャンクが返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/6036d23f-a9f6-9996-dbf4-b9fae7e9bc87.png)


次に、少しわざと間違えて「ファルトドメインとは」で検索しても返っています（さすがセマンティック検索）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/b2f047a8-1b0a-f01e-8586-56e7e2fd390d.png)

最後に、通称である「FDとは」で検索してもちゃんとフォルトドメインの内容が返ってきました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/84cfc0c7-0a7e-0e74-d23e-ea0363dc87a6.png)





# 終わりに
この記事を通じて、OCI OpenSearchのk-NNを用いたセマンティック検索の概念、セットアップ方法、そして具体的なデモ構築のプロセスについて詳しく学ぶことができました。
セマンティック検索は、今後の検索技術の進化において中核となる技術です。これを活用することで、より深いレベルでのデータ理解と情報抽出が可能になり、ユーザーにとってよりリッチで有益な検索結果を提供することができます。

実際に手を動かしながら学ぶことで、理論だけでは得られない貴重な知見や技術的な詳細を深く理解することができるでしょう。是非このデモを自分のプロジェクトに応用してみて、セマンティック検索の可能性を探求してください。また、このテクノロジーが今後どのように進化していくかを追い続けることで、常に最新の知識を保ち、技術の最前線で活躍することができます。

この記事が皆さんの学びの一助となれば幸いです。共に学び、共に成長していきましょう。