PythonとNeo4jで実現する全文検索に頼らないベクトルとグラフによるテキスト検索

# 1.今回の課題
発想の源は、プロジェクト完了報告書、人材情報、製品情報などの企業のナレッジや個人のナレッジをDBに登録して、DBから検索できないか？

その為に、膨大なテキストデータから全文検索を用いず、ベクトルとグラフ検索により意味で情報を取得できないかを試みる。

# 2.方式の説明
#### (1)Neo4jはグラフDBだが、これにベクトルも登録する。

#### (2)現在のベクトル検索の主流は文全体のベクトルを扱うが、今回はグラフも扱うので、ベクトルを算出する対象は主語、述語、目的語とする。

#### (3)データ登録スクリプトで、文の主語、目的語のノードに計算したベクトルもNeo4jに登録する。
:::note info
ノードに文の情報を登録すると、複数のノードと関連するごとに文の情報が上書きされ、古い情報が残らない可能性も考慮し、文の情報はエッジに登録する。
:::

#### (4)データ登録スクリプトで、文の述語のエッジに計算したベクトルと文の情報(ドキュメントファイル名、ファイル内の何番目の文か、文の内容)もNeo4jに登録する。

#### (5)データ検索スクリプトで、キーワードをベクトル変換し、そのベクトルにコサイン類似度で類似性の高いノード、エッジを検索し、ノードの場合、紐づくエッジも抽出する。そして、抽出したエッジから文の情報を取得する。
:::note info
ノードは文の情報を所持してないので
:::

# 3.使用技術の概要
前回の記事をベースとする。

https://qiita.com/mk1/items/fc5aa79eb5e2cb2c91ea

今回は使用ライブラリにsentence-transformers(Version:3.4.1)も使う。

# 4.DBへインプットするテキストデータ
前回の記事と同じ

```ner_data.txt
太郎は明子を好きです。
明子は次郎を好きです。
次郎は太郎の弟です。
AS社はソフトウェアを製造・販売している。
明子はAS社に勤めてます。
太郎はAS社からソフトウェアを3万円で買いました。
```

# 5.実装(データ登録スクリプト)と動作確認
#### (1)前回のソースからの変更、追加箇所
前回のtest.pyはsampleEntry.pyにリネーム

``` nlp.py
from sentence_transformers import SentenceTransformer

    # コンストラクタ
    def __init__(self):
        # NLPモデルの読み込み
        self.nlp_g = spacy.load("ja_ginza")

        # ベクトルモデルの読み込み
        self.model_vec = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')

    # ベクトル値の取得
    def getVec(self, word):
        return self.model_vec.encode(word)
```

#### (2)スクリプト実行時のダンプ
```
MERGE (n0:Person {name:'太郎'}) ON CREATE SET n0.vector=[-0.2106115072965622, -0.11719610542058945, 0.2813989520072937, 0.00996147096157074, -0.1804388165473938, 0.10936879366636276, 0.41256293654441833, 0.31228888034820557, 0.3112154006958008, 0.1385888010263443, 0.020292062312364578, 0.009421046823263168, -0.1905181109905243, -0.06253637373447418, 0.044488947838544846, -0.19789427518844604, -0.01980665884912014, -0.11420885473489761, 0.035298921167850494, 0.04272640496492386, 0.4232092797756195, -0.013531751930713654, 0.012020817957818508, -0.2357516884803772, -0.4452211856842041, -0.30947208404541016, -0.0013552280142903328, 0.3587712049484253, -0.18464040756225586, -0.3172908425331116, 0.20732975006103516, 0.12417087703943253, 0.1760205626487732, -0.23735861480236053, -0.001135135069489479, 0.2770546078681946, 0.012646045535802841, -0.18743261694908142, 0.0733693391084671, 0.274662047624588, 0.09410813450813293, -0.28002482652664185, -0.042828530073165894, 0.20990519225597382, -0.29970628023147583, -0.011200692504644394, 0.11947488784790039, -0.012052875012159348, 0.0022450496908277273, 0.28580695390701294, -0.07016704231500626, 0.2983679175376892, -0.2857915759086609, -0.29562920331954956, 0.30090171098709106, 0.12963223457336426, 0.1427115797996521, 0.1606137603521347, -0.2982638478279114, -0.006517490372061729, -0.1788012981414795, 0.49330824613571167, -0.5815352201461792, -0.07760810106992722, 0.05163995549082756, -0.19510489702224731, 0.10089409351348877, -0.24961990118026733, -0.10925478488206863, 0.02562079019844532, -0.22961992025375366, -0.18947023153305054, -0.08725348860025406, 0.15791986882686615, -0.16811414062976837, 0.013610891997814178, -0.20522522926330566, 0.23502205312252045, -0.16181445121765137, -0.4245959520339966, -0.42804500460624695, 0.005081175826489925, -0.08057728409767151, -0.10770393162965775, 0.004946935921907425, 0.19629475474357605, -0.03179384022951126, 0.12894007563591003, 0.552328884601593, 0.22428420186042786, 0.17483961582183838, -0.05577933043241501, 0.41140493750572205, 0.33211731910705566, 0.37893956899642944, -0.2503437101840973, 0.2463565468788147, -0.0448957160115242, -0.21340586245059967, 1.407063364982605, 0.13293689489364624, 0.025447798892855644, 0.007259072735905647, -0.2677081525325775, -0.0803259015083313, -0.031318750232458115, 0.026775624603033066, -0.5300508141517639, -0.4344254732131958, 0.21178597211837769, -0.013338793069124222, -0.12196996062994003, -0.12158681452274323, 0.06337612122297287, -0.15003348886966705, 0.11047447472810745, -0.2596037685871124, 0.2388259768486023, -0.20965686440467834, -0.27292072772979736, -0.19829151034355164, -0.2317531406879425, 0.22770173847675323, 0.3492024838924408, -0.043256159871816635, -0.5198546648025513, 0.35584843158721924, 0.11558285355567932, 0.14646866917610168, 0.22283835709095, 0.11227208375930786, 0.14859992265701294, -0.2699131965637207, 0.5551730394363403, -0.08578541874885559, -0.007958877831697464, 0.2998240292072296, 0.14008085429668427, -0.10139539837837219, -0.18924954533576965, -0.23706582188606262, 0.04098602011799812, -0.1589546501636505, 0.060914378613233566, 0.21303382515907288, -0.18952295184135437, -0.1332319676876068, -0.3685764670372009, -0.03557080402970314, 0.324972540140152, -0.084566131234169, -0.1936897337436676, -0.06593646854162216, 0.21488574147224426, 0.14681385457515717, -0.38307124376296997, 0.05852566659450531, 0.048914071172475815, 0.4902215898036957, 0.311909019947052, -0.3577539026737213, 0.1066463366150856, -0.07553469389677048, 0.07805828005075455, -0.3274804651737213, -0.14907848834991455, -0.5526140928268433, 0.18515834212303162, 0.04838185757398605, -0.11541759967803955, 0.4402194321155548, 0.18437038362026215, -0.40562593936920166, 0.07048608362674713, -0.032581478357315063, -0.22690677642822266, -0.2981760501861572, -0.0767308846116066, -0.18718907237052917, 0.4265635013580322, 0.16607539355754852, 0.1396556943655014, -0.3070601224899292, -0.05425009876489639, -0.04938943684101105, -0.18003308773040771, -0.013830989599227905, 0.22738149762153625, 0.24582061171531677, -0.1743190586566925, 0.11690639704465866, 0.04416750743985176, 0.1304713785648346, 0.10832177102565765, -0.24093860387802124, -0.20693626999855042, 0.3729458153247833, 0.17722785472869873, 0.305472195148468, 0.008574560284614563, -0.4364769160747528, 0.21684470772743225, -0.04368645325303078, -0.46985870599746704, 0.11968891322612762, -0.351434588432312, -0.13627874851226807, 0.05646679922938347, -0.003568503074347973, 0.16360357403755188, 0.25797051191329956, -0.29076066613197327, -0.410119891166687, 0.28796714544296265, 0.20315703749656677, 0.061739902943372726, 0.12573502957820892, -0.0013405987992882729, -0.233321875333786, 0.07262054085731506, 0.49020230770111084, -0.35200971364974976, -0.08292123675346375, -0.3514377772808075, 0.39799556136131287, -0.4775390326976776, -0.05955621227622032, -0.03551620617508888, -0.01084975153207779, -0.06957556307315826, 0.20427465438842773, 0.24723346531391144, -0.21056842803955078, -0.016282526776194572, -0.27533602714538574, -0.11686504632234573, 0.33741357922554016, -0.12384124100208282, 0.15946683287620544, 0.14047104120254517, -0.16098381578922272, -0.2432321310043335, 0.15501603484153748, 0.2475898265838623, -0.24153539538383484, -0.14339494705200195, -0.14386457204818726, 0.8434216976165771, -0.38257426023483276, 0.3437471389770508, 0.18606960773468018, -0.2954410910606384, -0.30164116621017456, 0.14969389140605927, 0.2932886481285095, 0.012421058490872383, -0.3854062855243683, 0.05551278218626976, 0.15192098915576935, 0.298009991645813, 0.3620585799217224, 0.03291294723749161, -0.20762214064598083, 0.21726280450820923, 0.40285342931747437, -0.23165030777454376, -0.007051646709442139, 0.024592041969299316, 0.02324795350432396, 0.17483706772327423, -0.15875282883644104, 0.11279016733169556, -0.29338538646698, -0.2662122845649719, 0.16358515620231628, -0.066329225897789, 0.2885802984237671, -0.5040294528007507, 0.11293739080429077, 0.34767383337020874, -0.3356795907020569, -0.2714221775531769, -0.042890749871730804, 0.20228731632232666, 0.0733591616153717, -0.2265622764825821, -0.16010285913944244, -0.30959320068359375, -0.1575392633676529, -0.11056043952703476, -0.03588081896305084, -0.050169751048088074, 0.08677853643894196, -0.0267029982060194, 0.12398207932710648, -0.3147507905960083, 0.12196502089500427, 0.5111790895462036, 0.07903558760881424, 0.04635661840438843, 0.06143385171890259, -0.5633187294006348, 0.32154759764671326, -0.36572423577308655, 0.06081700325012207, -0.20594295859336853, -0.20284518599510193, 0.004818633198738098, 0.01451256312429905, 0.15039724111557007, 0.07883056998252869, -0.32439708709716797, 0.07455694675445557, -0.041297830641269684, 0.09243437647819519, 0.36084529757499695, -0.014919927343726158, -0.09591460227966309, 0.6272990703582764, -0.7628309726715088, 0.20350372791290283, -0.16192665696144104, -0.13296152651309967, 0.5065715909004211, -0.2613382339477539, 0.059430260211229324, -0.007443413138389587, 0.10355095565319061, 0.04453269764780998, -0.07539539039134979, -0.17812573909759521, -0.05229005590081215, -0.08753401786088943, -0.07507027685642242, 0.26689058542251587, 0.19443626701831818, 0.01064140535891056, 0.40115588903427124, -0.07667845487594604, -0.0859241783618927, 0.39819878339767456, 0.0009236093610525131, 0.137945294380188, -0.3062558174133301, -0.19329452514648438, -0.06129182502627373, -0.13337482511997223, -0.2249523103237152, -0.10932992398738861, -0.32142695784568787, -0.04763332009315491, -0.02855922281742096, -0.22924354672431946, -0.23500192165374756, 0.13428659737110138, 0.4488615095615387, 0.1390974521636963, -0.06748539209365845, 0.5670657157897949, 0.17054009437561035, 0.3195047378540039, 0.08468650281429291, -0.13811880350112915, 0.000705081969499588, -0.11557428538799286, -0.025645887479186058, -0.28269368410110474, -0.04948686063289642, -0.06235674023628235, -0.06146681308746338, 0.04184674471616745, 0.27250391244888306, 0.26987382769584656, 0.12706494331359863, -0.04418202489614487, -0.013597292825579643, 0.07120731472969055, -0.06872153282165527, -0.06058391556143761, 0.3147452771663666, 0.376045823097229, -0.16350311040878296, -0.22595363855361938, -0.0
```

#### (3)Neo4jへの登録内容
ノード：vectorプロパティにベクトルが登録された。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989768/7d98b98f-0313-47d0-83f3-700e611af95c.png)

エッジ：ベクトル、テキスト情報が登録された。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989768/58c25737-21bb-4c9c-bd5d-5aec88f5fa92.png)

# 6.実装(データ検索スクリプト)と動作確認
#### (1)前回のソースからの変更、追加箇所
``` sampleSearch.py
from nlp import Nlp
from neo4jConnection import Neo4jConnection

if __name__ == "__main__":

    # 初期化
    instance_nlp = Nlp()
    instance_neo4j = Neo4jConnection()

    # DB接続
    if instance_neo4j.connect("bolt://localhost:7687", "neo4j", "xxxxxxxx"):
        # 検索Cypherの生成
        keyword = '弟'
        # keyword = '兄'
        # keyword = 'brother'
        # keyword = '愛'
        # keyword = 'love'
        # keyword = 'like'

        query_string = instance_nlp.makeCypherSearch(keyword, 0.7)
        print("データ検索Cypher\n" + query_string)

        # 検索
        answers = instance_neo4j.querySearch(query_string)

        # 検索結果出力
        print(keyword + 'による検索結果 start')
        for a in answers:
            print(a)
        print(keyword + 'による検索結果 end')

        # DB切断
        instance_neo4j.disConnect()
```

```nlp.py
    def makeCypherSearch(self, word, sim):
        vector = self.getVec(word)
        vector_str = np.array2string(vector, separator=',', precision=6).replace('\n', '').strip('[]')

        return f"""
        MATCH (n)-[r]-()
        WHERE n.vector IS NOT NULL AND r.vector IS NOT NULL
        WITH n, r, n.vector AS nodeVec, r.vector AS edgeVec, [{vector_str}] AS queryVec
        WITH n, r,
            reduce(s = 0.0, i IN range(0, size(nodeVec)-1) | s + nodeVec[i] * queryVec[i]) AS nodeDotProduct,
            sqrt(reduce(s = 0.0, i IN range(0, size(nodeVec)-1) | s + nodeVec[i]^2)) AS nodeMagnitude,
            reduce(s = 0.0, i IN range(0, size(edgeVec)-1) | s + edgeVec[i] * queryVec[i]) AS edgeDotProduct,
            sqrt(reduce(s = 0.0, i IN range(0, size(edgeVec)-1) | s + edgeVec[i]^2)) AS edgeMagnitude,
            sqrt(reduce(s = 0.0, i IN range(0, size(queryVec)-1) | s + queryVec[i]^2)) AS queryMagnitude
        WITH r, 
            edgeDotProduct / (edgeMagnitude * queryMagnitude) AS Similarity
        WHERE Similarity > {sim}
        RETURN DISTINCT r.FileName AS FileName, r.SntncNo AS SntncNo, r.SntncText AS SntncText, Similarity
        ORDER BY Similarity DESC
        """
```

```neo4jConnection.py
    def querySearch(self, query):
        with self._driver.session() as session:
            results = session.run(query)
            return [{'ファイル名':rec["FileName"], '何個目の文':rec["SntncNo"], '文の内容':rec['SntncText'], '類似性':rec['Similarity']} for rec in results]
```

#### (2)スクリプト実行時のダンプ
```類似性閾値0.7、キーワード「弟」の結果
弟による検索結果 start
{'ファイル名': 'ner_data.txt', '何個目の文': 3, '文の内容': '次郎は太郎の弟です。', '類似性': 0.9999999999999921}
弟による検索結果 end
```

```類似性閾値0.7、キーワード「兄」の結果
兄による検索結果 start
{'ファイル名': 'ner_data.txt', '何個目の文': 1, '文の内容': '太郎は明子を好きです。', '類似性': 0.7815191438724519}
{'ファイル名': 'ner_data.txt', '何個目の文': 2, '文の内容': '明子は次郎を好きです。', '類似性': 0.7815191438724519}
{'ファイル名': 'ner_data.txt', '何個目の文': 3, '文の内容': '次郎は太郎の弟です。', '類似性': 0.7741822889830346}
兄による検索結果 end
```

```類似性閾値0.7、キーワード「brother」の結果
brotherによる検索結果 start
{'ファイル名': 'ner_data.txt', '何個目の文': 3, '文の内容': '次郎は太郎の弟です。', '類似性': 0.9452192164060885}
brotherによる検索結果 end
```

```類似性閾値0.7、キーワード「愛」の結果
愛による検索結果 start
{'ファイル名': 'ner_data.txt', '何個目の文': 1, '文の内容': '太郎は明子を好きです。', '類似性': 0.7935586634102315}
{'ファイル名': 'ner_data.txt', '何個目の文': 2, '文の内容': '明子は次郎を好きです。', '類似性': 0.7935586634102315}
愛による検索結果 end
```

```類似性閾値0.7、キーワード「love」の結果
loveによる検索結果 start
loveによる検索結果 end
```

```類似性閾値0.7、キーワード「like」の結果
likeによる検索結果 start
likeによる検索結果 end
```

# 7.今回の課題の評価、今後の改善点
当初の目標を達成し、私が作りたい「テキスト検索サービス」のプロトタイプは作成できたと自己評価しています。
（ただし、今回は類似性の閾値を0.7と低めに設定しているので、これを0.8などに上げると取得できるテキストが8割減ります。）

実用化には、やはり、テキストの表記での検索との併用、検索Cypherの改良、他のDBを使う、DBでの情報の持ち方など、まだ改善すべき余地は残ってると感じています。

このプロトタイプに生成AIとの連携機能(質問＋DBからの検索結果を送り、回答を生成してもらう)を追加することで、RAGに発展できる可能性も感じています。

# 8. 最後に
これまでプライベートな時間で画像処理やAIを中心に探求してきましたが、
現在は電子工作、EDR、暗号化などの別の分野に心が惹かれています。
このように実務では経験できない分野を模索することで、技術の幅を広げていきたいと考えています。

以上