ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

elasticsearchでユーザベクトルを用いて検索する

ユーザベクトルとは、ユーザの最近の興味を表す数値からなるベクトルのことです。このベクトルを用いて検索できれば、検索結果にユーザの興味が反映されます。ここでは、ユーザベクトルによる検索をelasticsearchを用いて行う方法を書きます。

ユーザベクトルについて

ドキュメントをベクトル化する方法があると仮定します。例えば、tensorflow-hubのnnlmエンベディングを用いれば、ドキュメントをベクトル化することが可能です。

ユーザが検索をして、検索結果のある特定のリンクをクリックします。すると、クリックされたリンクのドキュメントベクトルはユーザベクトルの一部として保存されます。例えば、保存できるベクトルの件数を最新100件などとしておきます。

そして、検索をする際に「ユーザベクトルの平均ベクトル」と「ドキュメントベクトル」の類似度を使うようにすれば、ユーザの興味に類似した記事が検索可能です。

vector-scoringプラグイン

elasticsearch 5.6.0に対応したプラグインとして以下があります。 https://github.com/lior-k/fast-elasticsearch-vector-scoring

これは、ベクトルのbase64表現をインデクス登録することにより、ベクトルでスコアリングすることができるプラグインです。例えば、pythonでドキュメントベクトルを求めた場合、base64表現に変換するには以下の関数を使います。

import base64
import numpy as np

dbig = np.dtype('>f8')

def encode_array(arr):
    base64_str = base64.b64encode(np.array(arr).astype(dbig)).decode("utf-8")
    return base64_str

あとは、この関数でエンコーディングしたベクトルを以下のように登録するだけです:

{
    "id": 1,
    ....
    "embedding_vector": "v7l48eAAAAA/s4VHwAAAAD+R7I5AAAAAv8MBMAAAAAA/yEI3AAAAAL/IWkeAAAAAv7s480AAAAC/v6DUgAAAAL+wJi0gAAAAP76VqUAAAAC/sL1ZYAAAAL/dyq/gAAAAP62FVcAAAAC/tQRvYAAAAL+j6ycAAAAAP6v1KcAAAAC/bN5hQAAAAL+u9ItAAAAAP4ckTsAAAAC/pmkjYAAAAD+cYpwAAAAAP5renEAAAAC/qY0HQAAAAD+wyYGgAAAAP5WrCcAAAAA/qzjTQAAAAD++LBzAAAAAP49wNKAAAAC/vu/aIAAAAD+hqXfAAAAAP4FfNCAAAAA/pjC64AAAAL+qwT2gAAAAv6S3OGAAAAC/gfMtgAAAAD/If5ZAAAAAP5mcXOAAAAC/xYAU4AAAAL+2nlfAAAAAP7sCXOAAAAA/petBIAAAAD9soYnAAAAAv5R7X+AAAAC/pgM/IAAAAL+ojI/gAAAAP2gPz2AAAAA/3FonoAAAAL/IHg1AAAAAv6p1SmAAAAA/tvKlQAAAAD/I2OMAAAAAP3FBiCAAAAA/wEd8IAAAAL94wI9AAAAAP2Y1IIAAAAA/rnS4wAAAAL9vriVgAAAAv1QxoCAAAAC/1/qu4AAAAL+inZFAAAAAv7aGA+AAAAA/lqYVYAAAAD+kNP0AAAAAP730BiAAAAA="
}

ちなみに、embedding_vectorのテンプレートは以下のように定義します:

   {
        "embedding_vector": {
        "type": "binary",
        "doc_values": true
   }

検索の際には、以下のようなクエリを送信するだけです:

{
  "query": {
    "function_score": {
      "boost_mode": "replace",
      "script_score": {
        "lang": "knn",
        "params": {
          "cosine": false,
          "field": "embedding_vector",
          "vector": [
               -0.09217305481433868, 0.010635560378432274, -0.02878434956073761, 0.06988169997930527, 0.1273992955684662, -0.023723633959889412, 0.05490724742412567, -0.12124507874250412, -0.023694118484854698, 0.014595639891922474, 0.1471538096666336, 0.044936809688806534, -0.02795785665512085, -0.05665992572903633, -0.2441125512123108, 0.2755320072174072, 0.11451690644025803, 0.20242854952812195, -0.1387604922056198, 0.05219579488039017, 0.1145530641078949, 0.09967200458049774, 0.2161576747894287, 0.06157230958342552, 0.10350126028060913, 0.20387393236160278, 0.1367097795009613, 0.02070528082549572, 0.19238869845867157, 0.059613026678562164, 0.014012521132826805, 0.16701748967170715, 0.04985826835036278, -0.10990987718105316, -0.12032567709684372, -0.1450948715209961, 0.13585780560970306, 0.037511035799980164, 0.04251480475068092, 0.10693439096212387, -0.08861573040485382, -0.07457160204648972, 0.0549330934882164, 0.19136285781860352, 0.03346432000398636, -0.03652812913060188, -0.1902569830417633, 0.03250952064990997, -0.3061246871948242, 0.05219300463795662, -0.07879918068647385, 0.1403723508119583, -0.08893408626317978, -0.24330253899097443, -0.07105310261249542, -0.18161986768245697, 0.15501035749912262, -0.216160386800766, -0.06377710402011871, -0.07671763002872467, 0.05360138416290283, -0.052845533937215805, -0.02905619889497757, 0.08279753476381302
             ]
        },
        "script": "binary_vector_score"
      }
    }
  },
  "size": 100
}

サンプルコード

サンプルコードは以下で公開しました。 https://github.com/sugiyamath/personalized_search_example2

flaskでプロトタイプを作成し、app.pyは以下のように書いています:

#!/usr/bin/python3
# coding:utf-8

import sys
sys.path.append("../module")

import os
import re
import time
import numpy as np
import sqlite3
from elasticsearch import Elasticsearch
import search
from flask import Flask, jsonify, request, abort, render_template, make_response

db_root = "../db/"
app = Flask(__name__, template_folder='templates')
app.config['JSON_AS_ASCII'] = False
es = Elasticsearch("localhost:9202")
user_vector = []
user_vector.append([0.01 for x in range(128)])

@app.route('/search', methods=['GET'])
def search_it():
    global user_vector
    if not request.args:
        pages = []
        words = ['']
    else:
        query = request.args.get('q')
        qtype = request.args.get('qtype')
        if query.strip() == '' and qtype != '0':
            pages = []
            words = ['']
        else:
            conn = sqlite3.connect(os.path.join(db_root, 'mainichi.db'))
            if qtype == '1':
                terms = query.replace('%20',' ').replace(' ',' ')
                words = terms.split()
                pages = search.search(es, conn, terms, np.mean(user_vector,axis=0).tolist())
            elif qtype == '0':
                words = ['']
                pages = search.search_by_uv(es, conn, np.mean(user_vector, axis=0).tolist())

    return render_template('search.html', pages=pages, words=words)

@app.route('/redirect', methods=['GET'])
def redirect():
    global user_vector
    if not request.args:
        render_template('search.html', pages=[], words=[''])
    else:
        conn = sqlite3.connect(os.path.join(db_root, 'mainichi.db'))
        idx = request.args.get('id')
        url = request.args.get('url')
        article_vector = search.get_vector(conn, idx)
        user_vector.append(article_vector)
        user_vector.reverse()
        user_vector = user_vector[:10]
        user_vector.reverse()
        return render_template('redirect.html', url=url)

if __name__ == '__main__':
    app.run(debug=True, port=5504, host="0.0.0.0", threaded=True)

ただし、ユーザ数は1人を想定したものです。プロトタイプなので、多少コード的にマズイ部分もありますが、許してください。(例えば、グローバル変数の定義でflask.gを使っていないこと。)

参考

[1] https://github.com/lior-k/fast-elasticsearch-vector-scoring