Перейти к содержимому

Полнотекстовый поиск для качественного кодирования

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

При запуске сервера Potato строит полнотекстовый индекс по тексту каждого объекта, и администратор может искать по этому индексу в любом проекте, с QDA Mode или без него. Поиск по запросу copay возвращает каждый фрагмент интервью или открытый ответ из опроса, где есть слово, начинающееся с «copay». Результаты упорядочены по релевантности, а совпавшие слова подсвечены в коротком сниппете. Необязательная настройка search.annotator_claim добавляет на страницу кодирования панель Find. На панели Find кодировщик ищет по тому же индексу и забирает подходящие объекты в собственную очередь. Поиск и QDA Mode появились в Potato 2.6.0.

FTS5 — это расширение SQLite для полнотекстового поиска. SQLite — встроенная база данных, на которой работает хранилище проекта Potato. Potato хранит индекс как таблицу в <task_dir>/project.sqlite, и в том же файле лежат заметки и кодбук. Индекс содержит слова из объектов. Коды, заметки и разметка в индекс не входят, и поиск никогда не присваивает код найденным фрагментам.

В качественном проекте поиск решает две задачи:

  • Поиск фрагментов для кодирования. Кодировщик, который проходит корпус по темам, ищет «bus» или «copay», забирает объекты, где встречается слово, и кодирует нужные фрагменты схемой выделения span.
  • Проверка термина по всему корпусу. Аналитик или арбитр, который пишет критерии включения для кода, ищет термин, чтобы увидеть, в каких объектах термин встречается. Очередь ни одного кодировщика не меняется.

Настройка

Поиску не нужна настройка. Проект без блока search индексирует каждый объект при запуске и обслуживает поиск для администратора. Конфигурация ниже описывает полный проект в QDA Mode, в котором для кодировщика также включены поиск и взятие объектов:

yaml
annotation_task_name: Clinic access interviews
task_dir: .
output_annotation_dir: annotation_output/
data_files:
- data/interviews.json
item_properties:
  id_key: id
  text_key: text
admin_api_key: change-this-key
 
qda_mode:
  enabled: true
 
search:
  annotator_claim: true
 
annotation_schemes:
- annotation_type: span
  name: codes
  description: Highlight a passage and apply a code
  codebook: true
  labels: [access barriers, cost concerns, provider trust]

QDA Mode исходит из того, что весь корпус кодирует один кодировщик. При qda_mode.enabled: true Potato принимает annotator_claim при любой схеме назначения. Ключ admin_api_key позволяет вызывать поиск администратора из скрипта или командной строки.

Блок search принимает четыре ключа:

ПараметрПо умолчаниюОписание
search.enabledtrueСтроить индекс при запуске и обслуживать эндпоинты поиска. При false Potato не строит индекс, и поиск администратора отвечает 503.
search.backendfts5Движок индекса. В Potato 2.13.1 fts5 — единственный бэкенд. Любое другое значение отключает поиск и записывает предупреждение в лог.
search.max_instances100000Наибольшее число индексируемых объектов. Potato индексирует объекты в порядке загрузки, пока не достигнет предела. Объекты сверх предела в индекс не попадают, и лог сервера сообщает о пределе.
search.annotator_claimfalseПоказывать кодировщикам панель Find и обслуживать эндпоинты поиска и взятия для кодировщиков. Potato отказывается запускаться, если настройка конфликтует со схемой назначения (см. защиту назначения).

Кто может искать

Поиск администратора и поиск кодировщика читают один и тот же индекс через разные эндпоинты:

КтоЭндпоинтКогда доступенРезультатов на запрос
Администраторы и арбитрыGET /admin/api/search?q=<query>&limit=<n>search.enabled равно true50 по умолчанию, до 500
Любой вошедший кодировщикGET /api/search?q=<query>&limit=<n> и панель Findsearch.annotator_claim: true50 по умолчанию, до 200; панель Find запрашивает 50
Любой вошедший кодировщикPOST /api/search/claim с телом {"instance_id": "<id>"}search.annotator_claim: trueОдин объект на запрос

Доступ администратора означает либо ключ API администратора в заголовке X-API-Key, либо вошедшего пользователя, чья роль даёт доступ к панели администратора. В проекте с включённым блоком adjudication поиском администратора может пользоваться и арбитр. Поиск администратора работает только на чтение и не меняет ни одной очереди. В Potato 2.13.1 на панели администратора нет поля поиска, и администраторы ищут через эндпоинт:

bash
curl -H "X-API-Key: change-this-key" \
  "http://localhost:8000/admin/api/search?q=copay&limit=20"

Эндпоинт возвращает JSON такого вида:

json
{
  "count": 1,
  "query": "copay",
  "results": [
    {
      "id": "P01-03",
      "instance_id": "P01-03",
      "score": -0.525,
      "snippet": "My \u0002copay\u0003 went up again this year, so I skipped the follow…"
    }
  ]
}

Сниппет содержит до 12 слов вокруг наиболее совпадающей части объекта и отмечает каждое совпавшее слово управляющими символами U+0002 и U+0003. Панель Find превращает эти отметки в подсветку. score — это ранг BM25 в FTS5. Чем ниже значение, тем лучше совпадение, и результаты идут от лучшего совпадения к худшему. count — число возвращённых результатов. limit ограничивает это число, и ответ не сообщает, сколько ещё объектов совпадает сверх ограничения.

Синтаксис запросов

Potato разбивает запрос на каждом символе, который не является буквой, цифрой или подчёркиванием, и возвращает объекты, содержащие каждое получившееся слово как префикс. Potato никогда не передаёт запрос в FTS5 как синтаксис запроса. Правила ниже следуют из этого одного шага:

  • Каждое слово должно встретиться. bus line находит объект, где есть и «bus», и «line», в любом порядке и на любом расстоянии друг от друга.
  • Каждое слово сопоставляется как префикс. copay находит «copay», «copays» и «copayment». care находит «career» и «careful», а также «care».
  • Регистр и диакритика не учитываются. Copay и copay возвращают одни и те же объекты, а cafe находит «café».
  • Знаки препинания разделяют слова. self-care ищет «self» и «care». cost-of-care возвращает любой объект, где в любом месте текста есть «cost», «of» и «care».
  • Апострофы разделяют слова. couldn't ищет «couldn» и «t». Объект со словом «couldn't» находится, а сниппет подсвечивает ещё и другие слова на «t».
  • Операторы считаются обычными словами. Кавычки и * отбрасываются, а OR, NOT и NEAR становятся словами, которые должны быть в объекте. Запрос вроде cost OR care обычно ничего не возвращает.
  • Словоформы не объединяются. run находит «running» по правилу префикса, а ran никогда не находит «run». Ищите отдельно каждую форму, которую может использовать участник.

Индексируемый текст и перестроение индекса

Potato индексирует одну строку на объект и выбирает её в таком порядке:

  1. Поле, названное в item_properties.text_key (по умолчанию text).
  2. Список или объект под text_key, например транскрипт, сохранённый как список реплик. Potato индексирует JSON-текст, который показывает страница кодирования, вместе с именами полей.
  3. Для объекта без пригодного значения под text_key — первое непустое поле из caption, description, transcript, prompt, question, title, content и message.

Объекты без этих полей в индекс не попадают, и лог сервера сообщает, сколько объектов пропущено. Чаще всего это изображение без подписи. ID объектов, поля кейсов вроде participant_id и любые другие поля объекта не индексируются, поэтому поиск по ID участника ничего не находит. Кейсы группируют фрагменты по участникам.

Potato перестраивает весь индекс при каждом запуске сервера. Для исследования, загруженного из data_directory, Potato перестраивает индекс ещё раз после загрузки каталога, а наблюдатель за каталогом переиндексирует данные, когда приходят новые файлы. Если в SQLite из сборки Python нет FTS5, Potato оставляет поиск выключенным, эндпоинты поиска отвечают 503, а остальной Potato работает как обычно.

Поиск и взятие объектов

Панель Find

При search.annotator_claim: true на странице кодирования появляется кнопка Find. Панель Find принимает запрос и показывает до 50 совпадений, у каждого из которых есть сниппет, ID объекта и кнопка Claim. Claim добавляет объект в конец очереди кодировщика и заменяет кнопку надписью «In your queue». Взятие объекта, который уже стоит в очереди кодировщика, ничего не меняет, и эндпоинт взятия возвращает "already_assigned": true. Панель не открывает объект, и до взятого объекта кодировщик доходит, продвигаясь вперёд по очереди.

Эндпоинт взятия принимает любой ID объекта в проекте. Объект, который уже закреплён за другим кодировщиком, тоже можно взять. Неизвестный ID возвращает 404, запрос без сессии возвращает 401, а проект без annotator_claim возвращает 403.

Защита назначения

Кодировщик, который сам берёт объекты, выбирает выборку, а многие дизайны исследования зависят от того, что этот выбор делает Potato. При search.annotator_claim: true Potato отказывается запускаться, если в конфигурации также задано что-либо из следующего:

НастройкаПочему Potato отказывается запускаться
assignment_strategy со значением random, diversity_clustering, max_diversity, active_learning, llm_confidence, least_annotated, category_based, batch, psychometric, model_review или engineВыборка или порядок этой стратегии зависят от того, что каждый объект выбирает Potato.
num_annotators_per_item больше 1, overlap_sample.count больше 1 или старые ключи max_annotations_per_item и min_annotators_per_instance больше 1Potato не может гарантировать, что объекты перекрытия дойдут до нескольких кодировщиков.
attention_checks.enabled или gold_standards.enabledКодировщики могли бы найти проверочные объекты и пропустить их.
icl_labeling.enabledОбъекты слепой проверки модели стало бы можно найти.
adjudication.enabledОчередь арбитража формирует Potato.
crowdsourcing.provider с любым значением, кроме expert, или устаревший блок либо тип входа prolific или mturkЕдиницей назначения служит оплачиваемое задание, и взятие объектов ломает оплату и покрытие.

Защита допускает стратегии назначения fixed_order и priority, и fixed_order используется в Potato по умолчанию. Конфигурация с assignment_strategy: random и num_annotators_per_item: 2 не проходит проверку с таким сообщением:

text
ERRORS:
  - search.annotator_claim: true is incompatible with this configuration:
  - assignment_strategy: random (self-selection breaks sampling/ordering)
  - num_annotators_per_item: 2 (inter-annotator overlap cannot be guaranteed under self-selection)

При qda_mode.enabled: true или solo_mode.enabled: true Potato пропускает все проверки из таблицы выше. Поэтому командный проект в QDA Mode, который назначает выборку перекрытия для расчёта надёжности между кодировщиками, запускается с включённым взятием, и кодировщики могут брать объекты вне выборки, которую Potato назначил каждому из них. Пока команда кодирует выборку для оценки надёжности, оставляйте annotator_claim выключенным, а потом включите настройку, если самостоятельный выбор объектов кодировщиками подходит под дизайн исследования. Поиск администратора защита не затрагивает, потому что поиск администратора не меняет ни одной очереди.

Поиск во время кодирования в QDA Mode

Исследователь, который ищет термин в корпусе и затем кодирует каждый фрагмент, где термин встречается, выполняет в Potato такие шаги:

  1. Откройте Find и найдите термин, например transport. По правилу префикса найдутся также «transportation» и «transported».
  2. Прочитайте сниппеты и возьмите каждый объект, сниппет которого выглядит подходящим.
  3. Кодируйте каждый взятый объект, когда объект появится в вашей очереди. Выделите фрагмент и примените код из кодбука или нажмите i, чтобы создать код in vivo из выделения.
  4. Прикрепите заметку к объекту или выделению, чтобы записать, почему фрагмент подходит под код или почему почти подходящий фрагмент остаётся за пределами кода.

Поиск возвращает объекты, по одному сниппету на объект. Объект, где термин упоминается пять раз, появляется один раз, а целиком объект открывается только из очереди. В Potato 2.13.1 нет списка «ключевое слово в контексте» (keyword-in-context) со всеми вхождениями и нет шага, который применяет код ко всем совпадениям поиска. Каждое решение о кодировании остаётся за кодировщиком, который читает фрагмент.

Ограничения

  • Только текст объектов. Коды, заметки, разметка, ID объектов и поля, кроме индексируемого текста, недоступны для поиска.
  • Нет поиска по фразе, булевого, по близости или со стеммингом. Каждый запрос — набор префиксов, которые должны встретиться все.
  • Один сниппет на объект. Сниппет содержит до 12 слов, и объект появляется один раз, сколько бы раз ни встречался термин.
  • Ограниченные списки результатов. Поиск администратора возвращает не больше 500 объектов на запрос, поиск кодировщика не больше 200, а панель Find 50.
  • Языки, в которых слова не разделяются пробелами. Текст на китайском или японском индексируется как последовательности символов между знаками препинания, и запрос совпадает только с началом последовательности.
  • Медиа без текста. Объект с изображением, аудио или видео индексируется только через поле подписи или транскрипта, названное в text_key или найденное среди резервных полей.

Пример проекта из основного репозитория

В рабочей копии Potato запустите пример поиска из основного репозитория и обратитесь к эндпоинту администратора с ключом из примера:

bash
python potato/flask_server.py start examples/advanced/search-example/config.yaml -p 8000
curl -H "X-API-Key: search-example-key" \
  "http://localhost:8000/admin/api/search?q=rare"

Пример загружает шесть коротких отчётов о состоянии, и в трёх из них встречается «rare». Пример включает поиск и взятие объектов при назначении fixed_order с max_annotations_per_user: 2. Чтобы увидеть панель Find в проекте с фрагментами интервью, запустите вместо него пример QDA Mode из examples/advanced/qda-mode-example/config.yaml.

Что почитать дальше

Детали реализации см. в исходной документации.