Skip to content

akv17/tinysearch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Описание

Простой и компактный поисковый движок для быстрого поиска по небольшим корпусам текстов

Использование

$ python -m cli.app index config.yaml    # индексируем движок на корпусе
$ python -m cli.app search config.yaml   # выполняем поиск через CLI интерфейс
>>> ...
$ streamlit run streamlit_/app.py        # выполняем поиск через Streamlit интерфейс
>>> ...

Установка

$ python3.8 -m venv .venv
$ source .venv/bin/activate
$ pip install pip --upgrade
$ pip install setuptools --upgrade
$ pip install wheel
$ pip install -r requirements.txt

Особенности

  • Простое использование — всего две операции с простым интерфейсом
  • Простая разработка — легко добавить новый тип движка или корпуса
  • Просто понять — мало компонентов и мало кода
  • Поведение настраивается конфигом
  • Эффективная по времени и памяти реализация на базе NumPy, SciPy и PyTorch
  • Поддержка разных графических интерфейсов

Поисковые движки

BM25

Поиск с ранжированием по метрике Okapi BM25 в матричном виде.

Конфиг

corpus:                 # корпус для индексации
  type: "string"        # тип загрузчика корпуса
  params:               # параметры загрузчика корпуса 
    ...
engine:                 # поисковый движок
  dst: "string"         # путь для сохранения индекса
  type: "bm25"          # тип поискового движка
  params:               # параметры поискового движка
    preprocessor:       # препроцессор
      type: "string"    # тип препроцессора
      params:           # параметры препроцессора
        ...

BOW

Поиск по мешку слов в матричном виде.

Конфиг

corpus:                 # корпус для индексации
  type: "string"        # тип загрузчика корпуса
  params:               # параметры загрузчика корпуса 
    ...
engine:                 # поисковый движок
  dst: "string"         # путь для сохранения индекса
  type: "bow"           # тип поискового движка
  params:               # параметры поискового движка
    preprocessor:       # препроцессор
      type: "string"    # тип препроцессора
      params:           # параметры препроцессора
        ...
    vectorizer:         # векторизатор из sklearn
      type: "string"    # тип векторизатора
      params:           # параметры векторизатора
        ...

CTX

Поиск по контекстным векторам из предобученных моделей.

Конфиг

corpus:                   # корпус для индексации
  type: "string"          # тип загрузчика корпуса
  params:                 # параметры загрузчика корпуса 
    ...
engine:                   # поисковый движок
  dst: "string"           # путь для сохранения индекса
  type: "ctx"             # тип поискового движка
  params:                 # параметры поискового движка
    checkpoint: "string"  # чекпоинт модели из transformers

Примеры

CLI

Интерактивная сессия в терминале.
Позволяет искать по загруженному на старте сессии движку и индексу.
$ python -m cli.app search config.yaml

alt text

Streamlit

Веб-интерфейс на базе Streamlit.
Позволяет динамически выбирать движок и индекс и искать по нему.
$ streamlit run streamlit_/app.py

alt text

Архитектура

Примечание
Предложенную архитектуру не следует воспринимать как эталон или единственно верный вариант.
Это всего лишь один из многих возможных вариантов.

Предисловие

Зачем вообще возиться с архитектурой?
В первую очередь для того, чтобы обеспечить максимально комфортные условия для эффективной поддержки и развития приложения на максимально долгий период.
Выстраивание архитектуры не поможет вам сделать так, чтобы код работал согласно задумке, но поможет сделать так, чтобы этот код можно было поддерживать и развивать.

Обзор

В основе архитектуры лежат два компонента — поисковый движок и корпус.

Почему именно они?
Они обеспечивают главные функции нашего приложения — индексацию и поиск.
Корпус предоставляет доступ к документам для индексации, а движок — к операциям для индексации и поиска.

Что нужно уметь корпусу?

  • Загрузить документы для индексации по разной логике
  • Дать возможность узнать количество документов
  • Дать возможность итерации по документам
  • Дать возможность взять документ по айди

Что нужно уметь движку?

  • Создать экземпляр движка
  • Выполнить индексацию корпуса
  • Сохранить индекс на диск для дальнейшего переиспользования
  • Загрузить экземпляр движка из сохраненного индекса
  • Выполнить поиск по индексу

Перечисленных выше операций достаточно, чтобы реализовать на их базе весь нужный нам функционал.

Как мы обеспечим комфортное развитие приложения? Для начала обозначим, что мы хотим сделать удобным для изменений прежде всего.
Мы хотим иметь возможность легко добавлять новые типы поисковых движков и новую логику загрузки корпуса.
На деле это значит, что нам нужно сделать так, чтобы при изменении или расширении этих двух якорных компонентов, не ломались все остальные компоненты приложения.
Для этого зафиксируем интерфейс для движка и корпуса, через который все остальные компоненты приложения будут взаимодействовать с ними.
Так зависимые компоненты получат нечто вроде гаранта, что они не сломаются при добавлении или изменении внутренней логики тех компонентов, от которых они зависят.
Можно сказать, что так мы сделаем связи между компонентами стабильными, благодаря чему сможем изменять или добавлять отдельные компоненты, не боясь, что от этого как-то случайно сломаются или изменятся другие компоненты.

Что еще можно сделать?
Сразу на старте заложим еще несколько механизмов:

  1. Управление поведением с помощью конфига
  2. Разделение логики компонентов от логики создания их экземпляров
  3. Разделение логики приложения от логики отображения на интерфейсе

Чем поможет конфиг?
Внедрение конфига поможет управлять поведением приложения в понятном и стабильном виде.
С помощью конфига можно управлять большим количеством параметров и описывать логику практически любой сложности.
А еще конфиг помогает сделать запуск приложения проще и понятнее.

Чем поможет отделение логики компонента от логики его создания?
Речь о создании экземпляров классов.
Идея в том, чтобы не смешивать в одном классе логику его операций (его методы) и логику создания его экземпляров, а вынести логику создания экземпляров в отдельный класс или функцию.
Во-первых, часто получается так, что логика создания экземпляра класса не пересекается с логикой его операций, а значит нет особенного смысла связывать их вместе.
Во-вторых, иногда логика создания экземпляра может быть очень объемной или сложной, и ее определение внутри класса сделает его громоздким.
В нашем случае это, конечно, загрузка корпусов и загрузка движков из чекпоинтов.
В-третьих, подобно интерфейсам, такой подход поможет обеспечить стабильность между компонентами.
Суть в том, что так есть только одна "точка входа" для создания компонента, а значит все зависимые компоненты вынуждены использовать только ее.
Благодаря этому можно относительно независимо изменять как тот компонент, экземпляр которого создается, так и зависящие от него компоненты.
Этот прием соответствует паттерну проектирования Factory (фабрика).
Особенно этот паттерн полезен, когда логика создания экземпляров объемна и нетривиальна.
Помимо описанного выше, еще появляются бонусы:

  • Более чистый и компактный код класса
  • Возможность менять логику создания прямо в рантайме (а не только на старте приложения)
  • Возможность более безопасного и легкого расширения параметров класса

Чем поможет отделение логики от отображения?
Такое разделение внесет стабильность в связь между логикой и отображением.
Идея в том, что для того, чтобы отобразить результат поиска, не нужно знать в деталях о том, как работает поиск.
И наоборот, для того, чтобы провести ранжирование, не надо думать о том, как оно будет показано пользователю дальше.
Соблюдая этот простой принцип, мы сможем добавлять новые интерфейсы отображения без необходимости каких-либо изменений в логике поиска или индексации.
То же самое, конечно, справедливо и в обратную сторону.

Что общего в принципах выше?
Основная идея в том, чтобы постараться сделать взаимодействие между компонентами стабильным, прозрачным и предсказуемым и не создавать жесткие связи между компонентами.
Эффективный механизм для достижения этого — построение взаимодействия между компонентами, опираясь на их зафиксированные интерфейсы, а не на произвольные детали их логики.
Благодаря этому появляется возможность изменять компоненты независимо друг от друга без опасений случайно сломать какой-то другой компонент.

Детали

  • Основные компоненты
    1. Корпус
    2. Поисковый движок
    3. Препроцессор
    4. Выделенная публичная точка входа в приложение
  • Фиксированный интерфейс для каждого компонента
    1. ICorpusBuilder: загрузчик корпуса
    2. IEngine: поисковый движок
    3. IPreprocessor: препроцессор текста
  • Все взаимодействие между компонентами только через фиксированные интерфейсы
  • Приложение выполняет только то, что предоставляет публичная точка входа
    1. Загрузить корпус по конфигу
    2. Загрузить движок по конфигу
    3. Получить документ по айди
    4. Индексировать движок по конфигу и сохранить результат
  • Для создания экземпляров основных компонентов используем фабрики
  • Используем структуры данных для основных типов объектов
    1. Document: индексируемый документ
    2. Score: результат ранжирования документа
    3. Corpus: корпус документов
  • Поведение приложения и пути к данным управляются YAML конфигом
  • Графические интерфейсы реализованы отдельно от кода приложения и используют только то, что предоставлено публичной точкой входа

Структура файлов

  • assets: разные сопровождающие файлы
    • configs: конфиги
    • data: корпусы
  • cli: графический интерфейс на базе терминала
    • app.py: точка входа в интерфейс
    • mvc.py: логика интерфейса
  • streamlit_: графический интерфейс на базе Streamlit
    • app.py: точка входа в интерфейс
    • api.py: загрузка логики приложения
  • tinysearch: приложение
    • corpus: корпус и все связанное
      • factory.py: создание корпуса
      • multi_file.py: загрузчик корпуса из многих файлов
      • single_file.py: загрузчик корпуса из одного файла
    • engine: поисковые движки и все связанное
      • factory.py: создание поискового движка
      • bm25.py: движок на базе метрики BM25
      • bow.py: движок на базе мешка слов
      • ctx.py: движок на базе контекстных векторов
      • common: общий вспомогательный функционал
        • rank.py: ранжирование результатов поиска
    • preprocessor: препроцессоры и все связанное
      • factory.py: создание препроцессора
      • simple.py: простой препроцессор
    • api.py: публичная точка входа в приложение
    • data.py: структуры данных
    • interface.py: интерфейсы основных компонентов
  • .gitignore: файлы для игнорирования гитом
  • README.md: ридми
  • requirements.txt: зависимости

About

a tiny search engine

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages