SlideShare a Scribd company logo
1 of 70
Download to read offline
入門
自作検索エンジン
PyConJP 2019
自己紹介
加藤 遼(Ryo Kato)
@_ryook
サーバーサイド/API / 検索
python(Django)/Elasticsearch
宣伝
https://search-tech.connpass.com/
#searchtechjp
宣伝
スピーカー・会場/懇親会スポンサー募集中です
スピーカー応募フォーム スポンサー応募フォーム
この発表について
• 入門 自作検索エンジン
• 検索エンジンを自作することに入門した話
• 誰しも一度は検索エンジンを作りたいと思うはず
• 初めて検索エンジンを作るときの知見を一方的に共有するトーク
対象
• 全文検索のことを知らない / なんとなく知っている人
• 検索に興味がある人
• 検索エンジンを作ってみたい人
話さないこと
• Elasticsearch / Solrといった全文検索エンジンを使った検索アプリケーション
の話・pythonで使うtips
• 知りたい人はPyConJP2018の発表(※1)見てください or 検索技術勉強会へ
• もしくは是非空き時間に話しましょう
• クローリング / スクレイピングについて
• 気になる人は、本を読む or @shinyorkさん(※2) @vaaaaanquishさん(※3)のブログを見て
ください
• ※1: https://slideship.com/users/@iktakahiro/presentations/2018/09/DRJxjKfBFEGSRcvYjCA88f/
• ※2: https://shinyorke.hatenablog.com/entry/kowakunai-crawl-and-scraping
• ※3: https://vaaaaaanquish.hatenablog.com/entry/2017/06/25/202924
目次
• 全文検索について
• シンプルな検索エンジンを作る
• さらなる改善について
• まとめ
全文検索について
全文検索とは?
• 検索の対象が「テキストからなる文章の全部の文」である場合に、その文章に対
して検索を行うこと (※)
検索エンジンとは?
• 文章の集合から、単語や質問などからなる情報要求に適合する文章を見つける(検
索する)ためのシステムやソフトウェアの総称 (※)
※ 「検索エンジン自作入門」技術評論社
1分で作るpython製の検索エンジン
• 機能
• テキストの追加ができる
• キーワード検索ができる
1分で作るpython製の検索エンジン
def add_text(text: str):
    with open("db.txt", "a") as f:
        f.write(text + "n")
def search(keyword: str):
    with open("db.txt", "r") as f:
        return [l.strip() for l in f if keyword in l]
if __name__ == "__main__":
    texts = [
        "Beautiful is better than ugly.",
        "Explicit is better than implicit.",
        "Simple is better than complex."
    ]
    for text in texts:
        add_text(text)
    results = search("Simple")
    for result in results:
        print(result)
全文検索について
Grep型(※1)
• 線形走査を行う
• 現在のコンピューターでは、シェークスピア全集(約100万語)規模の文章に対
しての単純なクエリに対してはこれで充分という説もある(※2)
牽引(インデックス)型(※1)
• あらかじめ検索対象となる文書群を走査して牽引データを作っておく
ベクトル型
• 特徴ベクトルを作成してベクトル間の距離を計算
※1: https://ja.wikipedia.org/wiki/%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2
※2:「情報検索の基礎」共立出版
牽引(インデックス)型
• あらかじめ検索対象の文書を走査して、牽引データを準備しておく方法
• 牽引ファイルを作成することをインデクシング、生成されるデータをインデック
スという
• 牽引の作り方は様々あるが、一般的なのは転置インデックス
• 多くの全文検索エンジンで採用されている
• イメージは本の後ろについている牽引
• キーワードとページ
転置インデックス(逆インデックス)
• 単語とそれが含まれている文章のマッピングを保持するインデックス型のデータ
構造(※)
• 辞書とポスティングで構成される
Python
Elasticsearch
1, 2, 3, 5, 7, 10
1, 2, 10, 12, 15
辞書 ポスティング
ポスティングリスト
ポスティング
v
• ※: wikipedia
• ※図: 「情報検索の基礎」共立出版
v 転置インデックス
転置インデックスのインデックス単位
• レコード単位転置インデックス
• 単語と単語を含む文章(文章id)をリストして持つ
• i.g. Python: [1, 2, 3, 5, 7, 10]
• メリット: シンプルで実装しやすい。ディスク容量が少ない。
• デメリット: 機能性が乏しい
• 単語単位転置インデックス
• 単語と単語を含む文章(文章id)+ αの情報(i.g. 単語の出現位置)
• i.g. Python: [1:0, 2:1, 2:10, 7:1]
• メリット: 機能性が高い。例えばフレーズ検索ができる。
• デメリット: ディスク容量が多い。
• ※: 「情報検索の基礎」共立出版
転置インデックス
• pythonではdictionary
• keyが一致しなければ値は取得できない
• どういう単位でインデックスを作成するかが重要
転置インデックス
Python: [1, 2, 3, 5, 7, 10]
Python  -> ○
python  -> ✕
Py  -> ✕
パイソン  -> ✕
インデックスの抽出手法
• 単語分割(トークナイズ)
• White space
• 形態素解析(日本語)
• N-gram
• シグネチャ
• 接尾辞配列
• etc
インデックス生成
• インデックスの生成時にはトークナイズだけでなくテキストに処理を加える必要
もある
• どういう検索エンジンにするか依存する
• i.g. HTMLを検索するエンジン
• googleのようにコンテンツのみを対象にする場合
• HTMLタグは不要 →  HTMLタグを除去する
• Githubのようにコードを対象にする場合
• HTMLタグは必要
Analyzer処理
Char filter
Tokenizer
Token filter
<h1>Simple is better than complex.</h1>
simple is better than complex
[ simple , is , better , than , complex ]
[ simple , is , better->good , than , complex ]
[ simple , good , than , complex ]
Text
Tokens
Analyzer
• インデクシング処理
1.文書を受け取る
2.文書を分割する
1.文書全体に処理
2.トークン分割
3.トークンごとに処理
3.文章やトークン情報を保存
4.ポスティングリストを作成
5.転置インデックスを更新・保存
検索処理の流れ(1)
Analyzer
Indexer
Storage
Document
• 検索処理
1.クエリ受け取り
2.クエリパース
3.インデクシング時のtokenの形に揃
える
4.ポスティングリストを取得マージす
る
5.文書を取得する
6.並び替える
7.結果を返す
検索処理の流れ(2)
Parser
Analyzer
fetch
Sorter
Query
Results
Merge
シンプルな検索エンジンを作る
http://pycon-search-dev.ap-northeast-1.elasticbeanstalk.com/
シンプルな検索エンジンを作る
• 要件
• pyconjp 2019のトークを検索できる
• Titleと詳細
• 論理検索(and or not)に対応
• 結果はスコア(TFIDF)順に返す
• フレーズ検索は対応しない
• 複数フィールドの検索は対応しない
• ドキュメントの更新はしない
• 途中でドキュメントの追加をしない
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
全体像
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
Analyzer実装
• 英語と日本語それぞれのAnalyzerを作成
• 共通
• htmlタグ除外
• ストップワード除外
• アルファベットはすべて小文字に変換
• steaming処理
• i.g. dogs → dog
• 日本語
• 形態素解析分割
• 助詞・副詞・記号除外
• 英語
• White space分割
• 助詞等はストップワードで対応
Charfilter
• 共通のインターフェイスを作成
• 正規表現でhtmlタグを除去
• アルファベットをlowercaseに変換
class CharacterFilter:
    @classmethod
    def filter(cls, text: str):
        raise NotImplementedError
class HtmlStripFilter(CharacterFilter):
    @classmethod
    def filter(cls, text: str):
        html_pattern = re.compile(r"<[^>]*?>")
        return html_pattern.sub("", text)
class LowercaseFilter(CharacterFilter):
    @classmethod
    def filter(cls, text: str):
        return text.lower()
Tokenizer
• 共通のインターフェイスを準備
• 形態素解析にはJanomeを使用
• 注意
• JanomeのTokenizerオブジェクトの
初期化はコストが高いため,インスタ
ンスを使い回す。(※)
from janome.tokenizer import Tokenizer 
tokenizer = Tokenizer()
class BaseTokenizer:
    @classmethod
    def tokenize(cls, text):
        raise NotImplementedError
class JanomeTokenizer(BaseTokenizer):
    @classmethod
    def tokenize(cls, text):
        return (t for t in cls.tokenizer.tokeniz
e(text))
class WhitespaceTokenizer(BaseTokenizer):
    @classmethod
    def tokenize(cls, text):
        return (t[0] for t in re.finditer(r"[^ 
trn]+", text))
※1: https://github.com/mocobeta/janome
※2: https://mocobeta.github.io/janome/#tokenizer
Tokenfilter
• 共通のインターフェイスを準備
• POSFilterの都合上token引数はstrと
janomeのtokenオブジェクトを指定
STOPWORDS = ("is", "was", "to", "the")
def is_token_instance(token):
    return isinstance(token, Token)
class TokenFilter:
    @classmethod
    def filter(cls, token):
        """
        in: sting or janome.tokenizer.Token
        """
        raise NotImplementedError
class StopWordFilter(TokenFilter):
    @classmethod
    def filter(cls, token):
        if isinstance(token, Token):
            if token.surface in STOPWORDS:
                return None
        if token in STOPWORDS:
            return None
        return token
Tokenfilter
• Steming処理
• 単語の語幹を取り出す処理
• i.g. 「dogs」→「dog」
• nltk.stem パッケージを利用(※)
from nltk.stem.porter import PorterStemmer
ps = PorterStemmer()
class Stemmer(TokenFilter):
    @classmethod
    def filter(cls, token: str):
        if token:
            return ps.stem(token)
※http://www.nltk.org/api/nltk.stem.html
Tokenfilter
• POSFilter
• 特定の品詞を除外する
• janomeのtokenオブジェクトの
part_of_speechで判定
class POSFilter(TokenFilter):
    """
    日本語の助詞/記号を除くフィルター
    """
    @classmethod
    def filter(cls, token):
        """
        in: janome token
        """
        stop_pos_list = ("助詞", "副詞", "記号")
        if any([token.part_of_speech.startswith(pos) for
 pos in stop_pos_list]):
            return None
        return token
Analyzer
• Baseclass
• クラス変数で指定できるようにする
• tokenizer,
• char filer,
• token_filter
• filterは複数指定するため配列
• 前から順番に処理していく
class Analyzer:
    tokenizer = None
    char_filters = []
    token_filters = []
    @classmethod
    def analyze(cls, text: str):
        text = cls._char_filter(text)
        tokens = cls.tokenizer.tokenize(text)
        filtered_token = (cls._token_filter(token) for t
oken in tokens)
        return [parse_token(t) for t in filtered_token i
f t]
    @classmethod
    def _char_filter(cls, text):
        for char_filter in cls.char_filters:
            text = char_filter.filter(text)
        return text
    @classmethod
    def _token_filter(cls, token):
        for token_filter in cls.token_filters:
            token = token_filter.filter(token)
        return token
Analyzer
• 個別のAnalyzer
• 日本語用のJapaneseTokenizer
• 英語用のEnglishTokenizer
• かんたんに別のAnalyzerも作成可能
class JapaneseAnalyzer(Analyzer):
    tokenizer = JanomeTokenizer
    char_filters = [HtmlStripFilter, LowercaseFilter]
    token_filters = [StopWordFilter, POSFilter, Stemmer]
class EnglishAnalyzer(Analyzer):
    tokenizer = WhitespaceTokenizer
    char_filters = [HtmlStripFilter, LowercaseFilter]
    token_filters = [StopWordFilter, Stemmer]
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
Indexer
• 文章を受け取り転置インデックスを作成
• メモリ上に一時的な転置インデックス
(dictionay)で保持
• メモリ上に一定数の転置インデックスが
溜まったらストレージに保存
class InvertedIndex:
    def __init__(
        self, token_id: int, token: str, postings_list=[
], docs_count=0
    ) -> None:
        self.token_id = token_id
        self.token = token
        self.postings_list = []
        self.__hash_handle = {}
        self.docs_count = 0
def add_document(doc: str):
    """
    ドキュメントをデータベースに追加し転置インデックスを構築する
    """
    if not doc:
        return
    # # 文書IDと文章内容を基にミニ転置インデックス作成
    text_to_postings_lists(doc)
    # # # 一定数のドキュメントがミニ転置インデックスが溜まったら
マージ
    if len(TEMP_INVERT_INDEX) >= LIMIT:
        for inverted_index in TEMP_INVERT_INDEX.values()
:
            save_index(inverted_index)
Indexer
• ポスティングリストの作成
• analyzerでtokenを作成
• スコア計算のため文書中のトークン総数をあ
らかじめ計算しておく
• 「単語単位転置インデックス」作成
• フレーズ検索をしないため
• ただし、スコア計算のためそのトークンが文
書中にいくつあるか計算してインデックスに
もっておく
• Python: [doc1:2, doc2:1]
def text_to_postings_lists(text) -> list:
    """
    文章単位の転置リストを作る
    """
    tokens = JapaneseAnalyzer.analyze(text)
    token_count = len(tokens)
    document_id = save_document(text, token_count)
    cnt = Counter(tokens)
    for token, c in cnt.most_common():
        token_to_posting_list(token, document_id, c)
def token_to_posting_list(token: str, document_id: int, 
token_count: int):
    """
    tokenからposting listを作る
    """
    token_id = get_token_id(token)
    index = TEMP_INVERT_INDEX.get(token_id)
    if not index:
        index = InvertedIndex(token_id, token)
    posting = "{}:
{}".format(str(document_id), str(token_count))
    index.add_posting(posting)
    TEMP_INVERT_INDEX[token_id] = index
Indexer作成イメージ
• document
• doc1:「私は私です。」
• doc2:「私とpython。」
• Token
• token1: 私
• token2: python
• トークン数
• doc1: 2
• doc2: 2
• ポスティングリスト
• Python: [2:1]
• 私: [1:2, 2:1]
Indexerのポイント
• ポスティングリストはid順にソートする
• メモリ上の転置インデックスが一定数を超えたらストレージに保存
Indexerのポイント
• ポスティングリストはid順にソートする
• メモリ上の転置インデックスが一定数を超えたらストレージに保存
ポスティングリストはid順にソートする
• 検索の効率的に行うため
• 今回の実装では触れない
• i.g. スキップポインター(※)
• 今回はupdateしないのでappendするだけでOK
※: 「情報検索の基礎」共立出版
メモリとストレージの転置インデックス
• 全文検索エンジンで性能のボトルネックの多くをストレージのi/oが占める
• メモリ使用量と速度のトレードオフ
• メモリ上においておくほうが性能はいいがメモリ使用率が上がる
• ストレージに頻繁にアクセスするようにすればメモリ使用率は下がるが遅くな
る
※: 「情報検索の基礎」共立出版
※ 「検索エンジン自作入門」技術評論社
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
Storage
• 今回sqlite3 & sqlalchemyを使用
• なんでもいい
• NoSQLを使うと楽
• 今回は極力pythonだけ済むようにするためsqliteを採用
• より効率を求めるなら自分で実装する必要がある
Storage
• データベーススキーマ
• Documets tableにテキストを保存
• 検索対象のフィールドのテキストをtext
フィールドで保持しておく
class Documents(Base):
    __tablename__ = "documents"
    id = Column(Integer, primary_key=True)
    text = Column(String)
    token_count = Column(Integer)
    date = Column(String)
    time = Column(String)
    room = Column(String)
    title = Column(String)
    abstract = Column(String)
    speaker = Column(String)
    self_intro = Column(String)
    detail = Column(String)
    session_type = Column(String)
class Tokens(Base):
    __tablename__ = "tokens"
    id = Column(Integer, primary_key=True)
    token = Column(String)
class InvertedIndexDB(Base):
    __tablename__ = "index"
    id = Column(Integer, primary_key=True)
    token = Column(String)
    postings_list = Column(String)
    docs_count = Column(Integer)
    token_count = Column(Integer)
Storage
• データベース処理などのutils関数を実装
• トークンの追加・取得
• ドキュメントの追加・取得
• 転置インデックスの追加・更新・取得
def add_token(token: str) -> int:
    SESSION = get_session()
    token = Tokens(token=token)
    SESSION.add(token)
    SESSION.commit()
    token_id = token.id
    SESSION.close()
    return token_id
def fetch_doc(doc_id):
    SESSION = get_session()
    doc = SESSION.query(Documents).filter(Documents.id =
= doc_id).first()
    SESSION.close()
    if doc:
        return doc
    else:
        return None
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
merge / fetch
Sorter
Document Query Results
Searcher
• queryを受け取って
• クエリのパース(Parser / Analyzer)
• 結果のdoc_idを取得(Merger)
• 文章をストレージから取得(Fetcher)
• スコア順に並び替える(Sorter)
def search_by_query(query):
    if not query:
        return []
    # parse
    parsed_query = tokenize(query)
    parsed_query = analyzed_query(parsed_query)
    rpn_tokens = parse_rpn(parsed_query)
    # merge
    doc_ids, query_postings = merge(rpn_tokens)
    print(doc_ids, query_postings)
    # fetch
    docs = [fetch_doc(doc_id) for doc_id in doc_ids]
    # sort
    sorted_docs = sort(docs, query_postings)
    return [_parse_doc(doc) for doc, _ in sorted_docs]
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
Parser
• ユーザーの入力クエリをパースする
• AND, OR, NOTなどの論理検索に使うオペレーターや演算子を使う場合に必要
• 論理検索しないなら不要
• 今回は逆ポーランド記法に変換
逆ポーランド記法(後置記法)
• 演算子を被演算子の後ろに置く記法 (※)
• i.g.
• 「3 + 4」→「3 4 +」
• 「(3 + 4) * (1 - 2)」→ 「3 4 + 1 2 - *」
• 「python AND 検索」→ 「python 検索 AND」
• メリット
• 式の評価(計算)がシンプルになる
• 先頭から評価するだけで済む
• ※: wikipedia: https://ja.wikipedia.org/wiki/
%E9%80%86%E3%83%9D%E3%83%BC%E3%83%A9%E3%83%B3%E3%83%89%E8%A8%98%E6
%B3%95
操車場アルゴリズム詳細
• 逆ポーランド記法への変換は操車場アルゴリズムを利用
• アルゴリズムの詳細はwikipediaの操車場アルゴリズムのページを参照
• ※: wikipedia: https://ja.wikipedia.org/wiki/
%E6%93%8D%E8%BB%8A%E5%A0%B4%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E
3%82%BA%E3%83%A0
Parser
import re
REGEX_PATTERN = r"s*(d+|w+|.)"
SPLITTER = re.compile(REGEX_PATTERN)
LEFT = True
RIGHT = False
OPERATER = {"AND": (3, LEFT), "OR": (2, LEFT), "NOT": (1
, RIGHT)}
def tokenize(text):
    return SPLITTER.findall(text)
def parse_rpn(tokens: list):
   # アルゴリズムの実装
• アルゴリズムを実装
• 中置記法を後置記法にするだけなので実装
するのは文字列と演算子,括弧のみ
• 正規表現でスペースと括弧で分割
• i.g. [ A , AND , ( , B , OR , C , ) ]
• 利用するオペレーターと、その優先度、
結合方法を指定
• 優先度:
• NOT > OR > AND
• 結合
• AND, OR: 左結合
• NOT: 右結合
• 「A AND (B OR C)」
•  → ['A', 'B', 'C', 'OR', 'AND']
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
Analyzer
• 転置インデックス作成時に辞書に使っ
たtokenで検索する必要がある
• Index時に使ったAnalyzerをそのまま使っ
てもOK
• JapaneseTokenizerとEnglishTokenizer
に対応する
• 検索時には英単語で検索
• Whitespace tokenizerは考慮しない
• 今回は分割されたトークンをOR検索
として扱う
• 「機械学習 AND python」
• →「機械 OR 学習 AND python」
• 逆ポーランド記法変換前にAnalyzeす
る
def analyzed_query(parsed_query):
    return_val = []
    for q in parsed_query:
        if q in OPRS:
            return_val.append(q)
        else:
            analyzed_q = JapaneseAnalyzer.analyze(q)
            if analyzed_q:
                tmp = " OR ".join(analyzed_q)
                return_val += tmp.split(" ")
    return return_val
Merge
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
逆ポーランド記法評価
• 作成した逆ポーランド記法を評価していく
• 手順
• 「3 4 + 1 2 - *」:(3 + 4) * (1-2)の場合
1. (被演算子の場合) 演算子(3)をスタックに積む. stack: [3]
2. (被演算子の場合) 演算子(4)をスタックに積む: stack: [3, 4]
3. (演算子の場合) トップ2つをスタックから取り出す: stack[]
4. 計算(3+4)してスタックに積む: stack: [7]
5. (被演算子の場合) 演算子(1)をスタックに積む. stack: [7,1]
6. (被演算子の場合) 演算子(2)をスタックに積む: stack: [7,1,2]
7. (演算子の場合) トップ2つをスタックから取り出す: stack[7]
8. 計算(1- 2)してスタックに積む: stack: [7, -1]
9. (演算子の場合) トップ2つをスタックから取り出す: stack[]
10. 計算(7 * -1)してスタックに積む: stack: [-7]
逆ポーランド記法評価
• Merge = 逆ポーランド記法の評価
• 逆ポーランド記法の評価は3パターン
• 「スタックに積む」、「スタックから取り出す」、「計算する」
• 計算は2つの被演算子の評価ですむ
• 2つのトークンのポスティングリストをマージする
• スタックには、トークンから取得したポスティングリストを追加する
Merge
• 逆ポーランド記法評価手順通りに実装
• tokenが被演算子=トークンの場合に
ポスティングリストを取得後stockに
追加
• tokenが演算子の場合は、stockから
top2つを取り出してmerge後stock
に追加
• ポスティングリストはスコア計算よう
にdictionaryで管理
def merge(tokens: list):
    target_posting = {}
    stack = []
    for token in tokens:
        if token not in OPRS:
            token_id = get_token_id(token)
            postings_list = fetch_postings_list(token_id
)
            # score計算用に保持
            target_posting[token] = postings_list
            # doc_idのみをstackに追加
            doc_ids = set([p[0] for p in postings_list])
            stack.append(doc_ids)
        # tokenがoperaterだった場合
        else:
            if not stack:
                raise
            if len(stack) == 1:
                # NOTのみ許容
                if token == "NOT":
                    # NOTの処理
                    return not_doc_ids, {}
                else:
                    raise
            doc_ids1 = stack.pop()
            doc_ids2 = stack.pop()
            stack.append(merge_posting(token, doc_ids1, 
doc_ids2))
Merge
• 「NOT hoge」の対応
• 被演算子2つの評価以外の評価方法をNOT
のみ許可
• tokenが演算子の場合にstackのサイズと
tokenの中身で判定
# 「NOT hoge」対応
if len(stack) == 1:
    # NOTのみ許容
    if token == "NOT":
        # NOTの処理
        doc_ids = stack.pop()
        not_doc_ids = fetch_not_docs_id(doc_ids)
        return not_doc_ids, {}
    else:
        raise
Merge
• オペレーターをユーザーが記述しない
場合の対応
• 「python 機械学習」
• →「python OR 機械学習」?
• →「python AND 機械学習」?
• 今回はORを採用
• オペレーターを記述しない = 被演算子
と評価の数が合わない
• 最後まで評価してもstackが2つ以上ある
• 全てのトークンを実装したあとにstack
が1になるまでOR mergeを繰り返す
for token in tokens:
    # 評価処理
while len(stack) != 1:
    doc_ids1 = stack.pop()
    doc_ids2 = stack.pop()
    stack.append(merge_posting("OR", doc_ids1, doc_ids2)
)
Merge
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
シンプルな検索エンジンを作る
Analyzer
Indexer
Storage
Parser
Analyzer
Searcher(fetch / merge)
Sorter
Document Query Results
スコアリング
• TFIDF
• TF: term frequency ()
• 文書中の単語の割合
• 単語(t) / 文章中の単語数(T)
• iDF: invert document frequency
• 全文書中のその単語を含む文章の割合
• log(その単語を含む文書数(D)/全文書数(A))
• 文章中によく出てきて、他の文章にでてこないものほどスコアが高い
スコアリング
• 検索においてのIDF?
• 1キーワードだけで検索すると全文文書のIDFは同じ
• TFだけでよさそう
• ただし、AND, ORで検索したときに意味が変わってくる。
• 複数のクエリに対する文書郡の比較のための重み付けと考える
• 「A AND B」で検索したときの文書αのTFIDF値
• 文書αのTFIDF値 = クエリAのTFIDF + クエリBのTFIDF
Sorter
• スコア計算に使う値はほぼインデック
ス時に集計しているものを使う
• 1文書中の検索トークン数(t)
• postingで保持
• 1: 2
• 1文書中のトークン数(T)
• Documents保存時に一緒に保存
• doc.token_count
• 検索トークン(t)を含む文書数(D)
• tのポスティングリストの長さ
• 全文書数(A)
• Documents tableのカウント
def sort(doc_ids, query_postings):
    docs = []
    all_docs = count_all_docs()
    for doc_id in doc_ids:
        doc = fetch_doc(doc_id)
        doc_tfidf = 0
        for token, postings_list in query_postings.items
():
            idf = math.log10(all_docs / len(postings_lis
t)) + 1
            posting = [p for p in postings_list if p[0] 
== doc.id]
            if posting:
                tf = round(posting[0]
[1] / doc.token_count, 2)
            else:
                tf = 0
            token_tfidf = tf * idf
            doc_tfidf += token_tfidf
        docs.append((doc, doc_tfidf))
    return sorted(docs, key=lambda x: x[1], reverse=True
)
デプロイTips
• メモリを多めにする
• 性能があがる
• Janomeがpipのビルド時に500MB 600MB程度メモリが必要(※)
• herokuは厳しい
• free・hobyは✕
• Standardもおそらく✕
• EC2 t2.microも✕
• EC2 t2.smallは○
• ※ https://mocobeta.github.io/janome/
DEMO
DBのコード
DEMO
改善点
• インデクシング・検索の速度改善
• ポスティングリストの圧縮
• 効率のいいアルゴリズムとデータ構造を使う
• 動的なインデックスの作成
• クエリ拡張
• フレーズ検索
• 複数フィールド対応
• 演算子の拡張
• ストレージ部分(ファイルシステム)実装
• ベクトル検索
• 分散化
• etc
改善のための参考資料
• 書籍
• 「検索エンジン自作入門」技術評論社
• 「情報検索の基礎」共立出版
• 「高速文字列解析の世界」岩波書店
• 「DeepLearning for Search」MANNING
• OSS
• 「Whoosh」
• https://bitbucket.org/mchaput/whoosh/src/default/docs/
source/intro.rst
• 「Elasticsearch」
• https://github.com/elastic/elasticsearch
• 「Apache Lucene」
• http://lucene.apache.org/core/documentation.html
まとめ
• 検索エンジンの仕組みの説明から基本的な検索エンジンの実装まで説明した
• 単純に見える検索エンジンも実装すると色々考えることがある
• 勉強・趣味において車輪再発明は有意義
• 検索エンジンを作るのは楽しい
• みんなで「ぼくのかんがえたさいきょうの検索エンジン」を作りましょう

More Related Content

What's hot

フレッシャーズのためのパケット解析入門
フレッシャーズのためのパケット解析入門フレッシャーズのためのパケット解析入門
フレッシャーズのためのパケット解析入門彰 村地
 
MongoDB very basic (Japanese) / MongoDB基礎の基礎
MongoDB very basic (Japanese) / MongoDB基礎の基礎MongoDB very basic (Japanese) / MongoDB基礎の基礎
MongoDB very basic (Japanese) / MongoDB基礎の基礎Naruhiko Ogasawara
 
Learn Http Requests & Responses for Test Engineer
Learn Http Requests & Responses for Test EngineerLearn Http Requests & Responses for Test Engineer
Learn Http Requests & Responses for Test EngineerTakashi Moriyama
 

What's hot (6)

[BurpSuiteJapan]HTTP基礎入門
[BurpSuiteJapan]HTTP基礎入門[BurpSuiteJapan]HTTP基礎入門
[BurpSuiteJapan]HTTP基礎入門
 
フレッシャーズのためのパケット解析入門
フレッシャーズのためのパケット解析入門フレッシャーズのためのパケット解析入門
フレッシャーズのためのパケット解析入門
 
HTTP入門
HTTP入門HTTP入門
HTTP入門
 
MongoDB very basic (Japanese) / MongoDB基礎の基礎
MongoDB very basic (Japanese) / MongoDB基礎の基礎MongoDB very basic (Japanese) / MongoDB基礎の基礎
MongoDB very basic (Japanese) / MongoDB基礎の基礎
 
textsearch groonga v0.1
textsearch groonga v0.1textsearch groonga v0.1
textsearch groonga v0.1
 
Learn Http Requests & Responses for Test Engineer
Learn Http Requests & Responses for Test EngineerLearn Http Requests & Responses for Test Engineer
Learn Http Requests & Responses for Test Engineer
 

Similar to self made Fulltext search first_step

Code4Lib 2013参加報告
Code4Lib 2013参加報告Code4Lib 2013参加報告
Code4Lib 2013参加報告Masao Takaku
 
図書館でAPIをスルメのように 味わうには
図書館でAPIをスルメのように 味わうには図書館でAPIをスルメのように 味わうには
図書館でAPIをスルメのように 味わうにはTakanori Hayashi
 
実践!Elasticsearch + Sudachi を用いた全文検索エンジン
実践!Elasticsearch + Sudachi を用いた全文検索エンジン実践!Elasticsearch + Sudachi を用いた全文検索エンジン
実践!Elasticsearch + Sudachi を用いた全文検索エンジンS. T.
 
Pyconjp2016 pyftplib
Pyconjp2016 pyftplibPyconjp2016 pyftplib
Pyconjp2016 pyftplibShinya Okano
 
Go静的解析ハンズオン
Go静的解析ハンズオンGo静的解析ハンズオン
Go静的解析ハンズオンTakuya Ueda
 
IPアドレスコミュニティーのご紹介と最新の話題
IPアドレスコミュニティーのご紹介と最新の話題IPアドレスコミュニティーのご紹介と最新の話題
IPアドレスコミュニティーのご紹介と最新の話題Akira Nakagawa
 
Goにおける静的解析と製品開発への応用
Goにおける静的解析と製品開発への応用Goにおける静的解析と製品開発への応用
Goにおける静的解析と製品開発への応用Takuya Ueda
 
Twitterにおける即時話題推定技術「どたばたかいぎ」の開発
Twitterにおける即時話題推定技術「どたばたかいぎ」の開発Twitterにおける即時話題推定技術「どたばたかいぎ」の開発
Twitterにおける即時話題推定技術「どたばたかいぎ」の開発Eric Sartre
 
SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」
SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」 SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」
SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」 Yasushi Hara
 
Pythonが動く仕組み(の概要)
Pythonが動く仕組み(の概要)Pythonが動く仕組み(の概要)
Pythonが動く仕組み(の概要)Yoshiaki Shibutani
 
PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版
PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版
PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版Katsuhiro Morishita
 
20211213 インターネットの番号資源教室 akira
20211213 インターネットの番号資源教室 akira20211213 インターネットの番号資源教室 akira
20211213 インターネットの番号資源教室 akiraAkira Nakagawa
 
Python による 「スクレイピング & 自然言語処理」入門
Python による 「スクレイピング & 自然言語処理」入門Python による 「スクレイピング & 自然言語処理」入門
Python による 「スクレイピング & 自然言語処理」入門Tatsuya Tojima
 
Lab-ゼミ資料-5-20150512
Lab-ゼミ資料-5-20150512Lab-ゼミ資料-5-20150512
Lab-ゼミ資料-5-20150512Yuki Ogasawara
 
Infosta実習 発表ver
Infosta実習 発表verInfosta実習 発表ver
Infosta実習 発表verskdmai
 
学会講演要旨検索システムにおける タグクラウドによる 重要キーワードの可視化
学会講演要旨検索システムにおけるタグクラウドによる重要キーワードの可視化学会講演要旨検索システムにおけるタグクラウドによる重要キーワードの可視化
学会講演要旨検索システムにおける タグクラウドによる 重要キーワードの可視化Katsutoshi Seki
 
Elastic circle ci-co-webinar-20210127
Elastic circle ci-co-webinar-20210127Elastic circle ci-co-webinar-20210127
Elastic circle ci-co-webinar-20210127Shotaro Suzuki
 

Similar to self made Fulltext search first_step (20)

全文検索入門
全文検索入門全文検索入門
全文検索入門
 
Code4Lib 2013参加報告
Code4Lib 2013参加報告Code4Lib 2013参加報告
Code4Lib 2013参加報告
 
図書館でAPIをスルメのように 味わうには
図書館でAPIをスルメのように 味わうには図書館でAPIをスルメのように 味わうには
図書館でAPIをスルメのように 味わうには
 
実践!Elasticsearch + Sudachi を用いた全文検索エンジン
実践!Elasticsearch + Sudachi を用いた全文検索エンジン実践!Elasticsearch + Sudachi を用いた全文検索エンジン
実践!Elasticsearch + Sudachi を用いた全文検索エンジン
 
Web API入門
Web API入門Web API入門
Web API入門
 
Spotfire_8
Spotfire_8Spotfire_8
Spotfire_8
 
Pyconjp2016 pyftplib
Pyconjp2016 pyftplibPyconjp2016 pyftplib
Pyconjp2016 pyftplib
 
Go静的解析ハンズオン
Go静的解析ハンズオンGo静的解析ハンズオン
Go静的解析ハンズオン
 
IPアドレスコミュニティーのご紹介と最新の話題
IPアドレスコミュニティーのご紹介と最新の話題IPアドレスコミュニティーのご紹介と最新の話題
IPアドレスコミュニティーのご紹介と最新の話題
 
Goにおける静的解析と製品開発への応用
Goにおける静的解析と製品開発への応用Goにおける静的解析と製品開発への応用
Goにおける静的解析と製品開発への応用
 
Twitterにおける即時話題推定技術「どたばたかいぎ」の開発
Twitterにおける即時話題推定技術「どたばたかいぎ」の開発Twitterにおける即時話題推定技術「どたばたかいぎ」の開発
Twitterにおける即時話題推定技術「どたばたかいぎ」の開発
 
SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」
SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」 SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」
SciREX イノベーション分析手法勉強会 第8回 「SQL 入門と特許データベース分析(その2)」
 
Pythonが動く仕組み(の概要)
Pythonが動く仕組み(の概要)Pythonが動く仕組み(の概要)
Pythonが動く仕組み(の概要)
 
PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版
PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版
PythonとRによるデータ分析環境の構築と機械学習によるデータ認識 第3版
 
20211213 インターネットの番号資源教室 akira
20211213 インターネットの番号資源教室 akira20211213 インターネットの番号資源教室 akira
20211213 インターネットの番号資源教室 akira
 
Python による 「スクレイピング & 自然言語処理」入門
Python による 「スクレイピング & 自然言語処理」入門Python による 「スクレイピング & 自然言語処理」入門
Python による 「スクレイピング & 自然言語処理」入門
 
Lab-ゼミ資料-5-20150512
Lab-ゼミ資料-5-20150512Lab-ゼミ資料-5-20150512
Lab-ゼミ資料-5-20150512
 
Infosta実習 発表ver
Infosta実習 発表verInfosta実習 発表ver
Infosta実習 発表ver
 
学会講演要旨検索システムにおける タグクラウドによる 重要キーワードの可視化
学会講演要旨検索システムにおけるタグクラウドによる重要キーワードの可視化学会講演要旨検索システムにおけるタグクラウドによる重要キーワードの可視化
学会講演要旨検索システムにおける タグクラウドによる 重要キーワードの可視化
 
Elastic circle ci-co-webinar-20210127
Elastic circle ci-co-webinar-20210127Elastic circle ci-co-webinar-20210127
Elastic circle ci-co-webinar-20210127
 

Recently uploaded

論文紹介:Semantic segmentation using Vision Transformers: A survey
論文紹介:Semantic segmentation using Vision Transformers: A survey論文紹介:Semantic segmentation using Vision Transformers: A survey
論文紹介:Semantic segmentation using Vision Transformers: A surveyToru Tamaki
 
TSAL operation mechanism and circuit diagram.pdf
TSAL operation mechanism and circuit diagram.pdfTSAL operation mechanism and circuit diagram.pdf
TSAL operation mechanism and circuit diagram.pdftaisei2219
 
TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案
TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案
TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案sugiuralab
 
SOPを理解する 2024/04/19 の勉強会で発表されたものです
SOPを理解する       2024/04/19 の勉強会で発表されたものですSOPを理解する       2024/04/19 の勉強会で発表されたものです
SOPを理解する 2024/04/19 の勉強会で発表されたものですiPride Co., Ltd.
 
論文紹介:Automated Classification of Model Errors on ImageNet
論文紹介:Automated Classification of Model Errors on ImageNet論文紹介:Automated Classification of Model Errors on ImageNet
論文紹介:Automated Classification of Model Errors on ImageNetToru Tamaki
 
論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...
論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...
論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...Toru Tamaki
 
【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)
【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)
【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)Hiroki Ichikura
 
Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介
Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介
Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介Yuma Ohgami
 

Recently uploaded (8)

論文紹介:Semantic segmentation using Vision Transformers: A survey
論文紹介:Semantic segmentation using Vision Transformers: A survey論文紹介:Semantic segmentation using Vision Transformers: A survey
論文紹介:Semantic segmentation using Vision Transformers: A survey
 
TSAL operation mechanism and circuit diagram.pdf
TSAL operation mechanism and circuit diagram.pdfTSAL operation mechanism and circuit diagram.pdf
TSAL operation mechanism and circuit diagram.pdf
 
TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案
TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案
TataPixel: 畳の異方性を利用した切り替え可能なディスプレイの提案
 
SOPを理解する 2024/04/19 の勉強会で発表されたものです
SOPを理解する       2024/04/19 の勉強会で発表されたものですSOPを理解する       2024/04/19 の勉強会で発表されたものです
SOPを理解する 2024/04/19 の勉強会で発表されたものです
 
論文紹介:Automated Classification of Model Errors on ImageNet
論文紹介:Automated Classification of Model Errors on ImageNet論文紹介:Automated Classification of Model Errors on ImageNet
論文紹介:Automated Classification of Model Errors on ImageNet
 
論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...
論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...
論文紹介:Content-Aware Token Sharing for Efficient Semantic Segmentation With Vis...
 
【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)
【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)
【早稲田AI研究会 講義資料】3DスキャンとTextTo3Dのツールを知ろう!(Vol.1)
 
Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介
Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介
Open Source UN-Conference 2024 Kawagoe - 独自OS「DaisyOS GB」の紹介
 

self made Fulltext search first_step