CTX_DOC в PL/SQL. Введение
Пакет CTX_DOC — набор сервисов представления документов Oracle Text:
FILTER, MARKUP, HIGHLIGHT, SNIPPET, GIST, THEMES, TOKENS,
а также POLICY_* версии (без предварительного индекса), PKENCODE, SET_KEY_TYPE,
LANGUAGES/STEMS/NOUN_PHRASES/PART_OF_SPEECH и SENTIMENT.
Введение
Ниже — 50 практических примеров: подготовка таблиц и индекса, фильтрация и разметка,
подсветка совпадений и сниппеты, темы и токены, policy-варианты без индекса, анализ языка и тональности,
а также лучшие практики (forward index, save copy). Во всех PL/SQL-блоках нет строки / после END;.
Подготовка демо-данных и базовой конфигурации
CREATE TABLE docs (
id NUMBER PRIMARY KEY,
text CLOB
);INSERT INTO docs (id, text) VALUES (1, 'New CPU and GPU benchmarks for gaming laptops with graphics cards.');
INSERT INTO docs (id, text) VALUES (2, 'Order pizza, burgers, and fries — fast food menu and restaurant deals.');
INSERT INTO docs (id, text) VALUES (3, 'Healthy vegan bowls, salads and fresh ingredients for diet.');
COMMIT;BEGIN
CTX_DDL.CREATE_PREFERENCE('lex_en','BASIC_LEXER');
CTX_DDL.SET_ATTRIBUTE('lex_en','MIXED_CASE','YES');
CTX_DDL.CREATE_PREFERENCE('wl_en','BASIC_WORDLIST');
CTX_DDL.SET_ATTRIBUTE('wl_en','STEMMER','ENGLISH');
CTX_DDL.CREATE_PREFERENCE('stop_en','BASIC_STOPLIST');
CTX_DDL.ADD_STOPWORD('stop_en','and');
CTX_DDL.CREATE_SECTION_GROUP('sg_html','HTML_SECTION_GROUP');
CTX_DDL.ADD_FIELD_SECTION('sg_html','title','TITLE', TRUE);
END;CREATE INDEX idx_docs ON docs(text)
INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS (
'LEXER lex_en WORDLIST wl_en STOPLIST stop_en SECTION GROUP sg_html SAVE COPY FILTERED'
);BEGIN
-- Простая политика для policy_* процедур (без предварительного индекса)
CTX_DDL.CREATE_POLICY(
policy_name => 'p_text',
filter => NULL,
section_group => 'SG_HTML',
lexer => 'LEX_EN',
stoplist => 'STOP_EN',
wordlist => 'WL_EN'
);
END;50 примеров по CTX_DOC
1. FILTER: получить HTML-версию документа в CLOB (in-memory)
DECLARE
outdoc CLOB;
BEGIN
CTX_DOC.FILTER('IDX_DOCS', '1', outdoc, plaintext => FALSE);
DBMS_OUTPUT.PUT_LINE('Length='||DBMS_LOB.GETLENGTH(outdoc));
DBMS_LOB.FREETEMPORARY(outdoc);
END;2. FILTER: получить plaintext-версию документа в таблицу
CREATE TABLE filtertab (query_id NUMBER, document CLOB);
BEGIN
CTX_DOC.FILTER('IDX_DOCS', '2', 'FILTERTAB', 1, plaintext => TRUE);
END;3. FILTER: использовать сохранённую копию (SAVE COPY)
DECLARE
outdoc CLOB;
BEGIN
CTX_DOC.FILTER('IDX_DOCS', '1', outdoc, plaintext => TRUE,
use_saved_copy => CTX_DOC.SAVE_COPY_FALLBACK);
DBMS_LOB.FREETEMPORARY(outdoc);
END;4. POLICY_FILTER: фильтрация без индекса по политике
DECLARE
outdoc CLOB;
in_text CLOB := 'Sample <b>HTML</b> with tags and entities & symbols.';
BEGIN
CTX_DOC.POLICY_FILTER('P_TEXT', in_text, outdoc, plaintext => TRUE);
DBMS_LOB.FREETEMPORARY(outdoc);
END;5. IFILTER: фильтровать BLOB (заменяется POLICY_FILTER)
DECLARE
b BLOB; t CLOB;
BEGIN
DBMS_LOB.CREATETEMPORARY(t, TRUE);
-- b должен содержать бинарный документ (PDF/DOC/XLS и т.п.)
CTX_DOC.IFILTER(b, t);
DBMS_LOB.FREETEMPORARY(t);
END;6. FILTER: фильтрация с query_id в таблицу
CREATE TABLE filtertab2 (query_id NUMBER, document CLOB);
BEGIN
CTX_DOC.FILTER('IDX_DOCS', '3', 'FILTERTAB2', 1001, plaintext => FALSE);
END;7. FILTER: через ROWID (SET_KEY_TYPE)
DECLARE
outdoc CLOB;
BEGIN
CTX_DOC.SET_KEY_TYPE('ROWID');
CTX_DOC.FILTER('IDX_DOCS', (SELECT ROWID FROM docs WHERE id=1), outdoc, TRUE);
CTX_DOC.SET_KEY_TYPE('PRIMARY KEY');
DBMS_LOB.FREETEMPORARY(outdoc);
END;8. FILTER: composite PK через PKENCODE
DECLARE
outdoc CLOB;
BEGIN
-- если бы PK был составным:
-- vkey := CTX_DOC.PKENCODE('A', 'B', 'C');
-- CTX_DOC.FILTER('IDX_DOCS', vkey, outdoc);
NULL;
END;9. POLICY_FILTER: обработка BFILE
DECLARE
outdoc CLOB;
bf BFILE := BFILENAME('DOCS_DIR','manual.html');
BEGIN
CTX_DOC.POLICY_FILTER('P_TEXT', bf, outdoc, plaintext => TRUE);
DBMS_LOB.FREETEMPORARY(outdoc);
END;10. FILTER: аварийное поведение при отсутствии SAVE COPY
DECLARE
outdoc CLOB;
BEGIN
CTX_DOC.FILTER('IDX_DOCS', '2', outdoc, plaintext => TRUE,
use_saved_copy => CTX_DOC.SAVE_COPY_ERROR);
DBMS_LOB.FREETEMPORARY(outdoc);
END;11. MARKUP: разметка совпадений (HTML)
DECLARE
html_out CLOB;
BEGIN
CTX_DOC.MARKUP('IDX_DOCS', '1', 'gpu AND cpu', html_out, plaintext => FALSE, tagset => 'HTML_DEFAULT');
DBMS_LOB.FREETEMPORARY(html_out);
END;12. MARKUP: разметка по теме (ABOUT)
DECLARE
html_out CLOB;
BEGIN
CTX_DOC.MARKUP('IDX_DOCS', '1', 'about(computers)', html_out, plaintext => FALSE, tagset => 'HTML_DEFAULT');
DBMS_LOB.FREETEMPORARY(html_out);
END;13. MARKUP_CLOB_QUERY: передать запрос CLOB
DECLARE
q CLOB;
out CLOB;
BEGIN
DBMS_LOB.CREATETEMPORARY(q, TRUE);
DBMS_LOB.WRITEAPPEND(q, LENGTH('pizza OR burgers'), 'pizza OR burgers');
CTX_DOC.MARKUP_CLOB_QUERY('IDX_DOCS','2', q, out, plaintext => TRUE, tagset => 'TEXT_DEFAULT');
DBMS_LOB.FREETEMPORARY(q);
DBMS_LOB.FREETEMPORARY(out);
END;14. POLICY_MARKUP: без индекса по политике
DECLARE
out CLOB;
txt CLOB := 'Vegan bowls and salads — tasty and healthy';
BEGIN
CTX_DOC.POLICY_MARKUP('P_TEXT', txt, 'vegan AND salads', out, plaintext => TRUE, tagset => 'TEXT_DEFAULT');
DBMS_LOB.FREETEMPORARY(out);
END;15. MARKUP: навигационный tagset
DECLARE
out CLOB;
BEGIN
CTX_DOC.MARKUP('IDX_DOCS','1','gpu', out, plaintext => FALSE, tagset => 'HTML_NAVIGATE');
DBMS_LOB.FREETEMPORARY(out);
END;16. MARKUP: кастомные start/end теги
DECLARE
out CLOB;
BEGIN
CTX_DOC.MARKUP('IDX_DOCS','2','pizza', out, plaintext => TRUE,
starttag => '[HIT]', endtag => '[/HIT]');
DBMS_LOB.FREETEMPORARY(out);
END;17. MARKUP: игнор фильтров-предикатов
DECLARE
out CLOB;
BEGIN
-- SDATA/HASPATH/INPATH-предикаты игнорируются в MARKUP/HIGHLIGHT/SNIPPET
CTX_DOC.MARKUP('IDX_DOCS','1','gpu AND SDATA(price > 100)', out, tagset => 'HTML_DEFAULT');
DBMS_LOB.FREETEMPORARY(out);
END;18. MARKUP: plaintext-вывод
DECLARE
out CLOB;
BEGIN
CTX_DOC.MARKUP('IDX_DOCS','3','diet OR healthy', out, plaintext => TRUE, tagset => 'TEXT_DEFAULT');
DBMS_LOB.FREETEMPORARY(out);
END;19. HIGHLIGHT: получить оффсеты совпадений (in-memory)
DECLARE
t CTX_DOC.HIGHLIGHT_TAB;
BEGIN
CTX_DOC.HIGHLIGHT('IDX_DOCS','1','gpu', t, plaintext => TRUE);
FOR i IN 1..t.COUNT LOOP
DBMS_OUTPUT.PUT_LINE('offset='||t(i).offset||' len='||t(i).length);
END LOOP;
END;20. HIGHLIGHT: HTML-оффсеты для применения к FILTER(HTML)
DECLARE
t CTX_DOC.HIGHLIGHT_TAB;
BEGIN
CTX_DOC.HIGHLIGHT('IDX_DOCS','1','about(computers)', t, plaintext => FALSE);
-- применить t к HTML из FILTER (см. пример 1)
END;21. POLICY_HIGHLIGHT: без индекса
DECLARE
t CTX_DOC.HIGHLIGHT_TAB;
txt CLOB := 'Order pizza and fries for dinner';
BEGIN
CTX_DOC.POLICY_HIGHLIGHT('P_TEXT', txt, 'pizza', t, plaintext => TRUE);
DBMS_OUTPUT.PUT_LINE('hits='||t.COUNT);
END;22. HIGHLIGHT: учёт регистров с лексером
DECLARE
t CTX_DOC.HIGHLIGHT_TAB;
BEGIN
CTX_DOC.HIGHLIGHT('IDX_DOCS','2','Pizza', t, plaintext => TRUE);
DBMS_OUTPUT.PUT_LINE('hits='||t.COUNT);
END;23. HIGHLIGHT: длинный запрос (CLOB)
DECLARE
q CLOB; t CTX_DOC.HIGHLIGHT_TAB;
BEGIN
DBMS_LOB.CREATETEMPORARY(q, TRUE);
DBMS_LOB.WRITEAPPEND(q, 20, 'pizza OR burgers OR fries');
CTX_DOC.HIGHLIGHT_CLOB_QUERY('IDX_DOCS','2', q, t, plaintext => TRUE);
DBMS_LOB.FREETEMPORARY(q);
END;24. HIGHLIGHT: UCS-2 оффсеты — безопасная нарезка
DECLARE
t CTX_DOC.HIGHLIGHT_TAB;
s CLOB;
BEGIN
CTX_DOC.FILTER('IDX_DOCS','2', s, plaintext => TRUE);
CTX_DOC.HIGHLIGHT('IDX_DOCS','2','pizza', t, plaintext => TRUE);
-- при нарезке по оффсетам учитывайте UCS-2 кодпоинты
DBMS_LOB.FREETEMPORARY(s);
END;25. HIGHLIGHT: запись результатов в таблицу
CREATE TABLE highlighttab (query_id NUMBER, offset NUMBER, length NUMBER);
BEGIN
CTX_DOC.HIGHLIGHT('IDX_DOCS','1','gpu', 'HIGHLIGHTTAB', 42, plaintext => TRUE);
END;26. POLICY_HIGHLIGHT: HTML-оффсеты
DECLARE
t CTX_DOC.HIGHLIGHT_TAB;
txt CLOB := '<p>Fast <b>GPU</b> and CPU in laptop</p>';
BEGIN
CTX_DOC.POLICY_HIGHLIGHT('P_TEXT', txt, 'gpu AND cpu', t, plaintext => FALSE);
DBMS_OUTPUT.PUT_LINE('HTML hits='||t.COUNT);
END;27. SNIPPET: получить сниппет с подсветкой (in-memory)
DECLARE
snippet CLOB;
BEGIN
CTX_DOC.SNIPPET('IDX_DOCS','1','gpu OR cpu', snippet, starttag => '<b>', endtag => '</b>');
DBMS_LOB.FREETEMPORARY(snippet);
END;28. SNIPPET: сохранить в таблицу
CREATE TABLE snippettab (query_id NUMBER, snippet CLOB);
BEGIN
CTX_DOC.SNIPPET('IDX_DOCS','2','pizza', 'SNIPPETTAB', 7, starttag => '<b>', endtag => '</b>');
END;29. POLICY_SNIPPET: без индекса
DECLARE
out CLOB;
txt CLOB := 'Healthy vegan bowls are tasty and good for diet';
BEGIN
CTX_DOC.POLICY_SNIPPET('P_TEXT', txt, 'vegan AND diet', out, starttag => '<b>', endtag => '</b>');
DBMS_LOB.FREETEMPORARY(out);
END;30. SNIPPET: поведение с XML секциями
-- При XML_SECTION_GROUP пользовательские теги удаляются,
-- структура игнорируется, возможны "захваты" соседнего текста.
-- Используйте NULL_SECTION_GROUP для предсказуемой выдачи сниппетов.31. SNIPPET: ограничить длину фрагментов (separator)
DECLARE
out CLOB;
BEGIN
CTX_DOC.SNIPPET('IDX_DOCS','1','gpu', out, starttag => '<b>', endtag => '</b>', separator => '<b>…</b>');
DBMS_LOB.FREETEMPORARY(out);
END;32. SNIPPET: с учётом SAVE COPY PLAINTEXT
DECLARE
out CLOB;
BEGIN
CTX_DOC.SNIPPET('IDX_DOCS','3','salads OR bowls', out);
DBMS_LOB.FREETEMPORARY(out);
END;33. SNIPPET: по длинному запросу (CLOB)
DECLARE
q CLOB; out CLOB;
BEGIN
DBMS_LOB.CREATETEMPORARY(q, TRUE);
DBMS_LOB.WRITEAPPEND(q, 50, 'salads OR bowls OR fresh OR ingredients');
CTX_DOC.SNIPPET_CLOB_QUERY('IDX_DOCS','3', q, out, starttag => '[', endtag => ']');
DBMS_LOB.FREETEMPORARY(q);
DBMS_LOB.FREETEMPORARY(out);
END;34. SNIPPET: HTML против PLAINTEXT
DECLARE
out_html CLOB; out_text CLOB;
BEGIN
CTX_DOC.SNIPPET('IDX_DOCS','2','pizza', out_html, plaintext => FALSE);
CTX_DOC.SNIPPET('IDX_DOCS','2','pizza', out_text, plaintext => TRUE);
DBMS_LOB.FREETEMPORARY(out_html);
DBMS_LOB.FREETEMPORARY(out_text);
END;35. THEMES: single themes в память
DECLARE
t CTX_DOC.THEME_TAB;
BEGIN
CTX_DOC.THEMES('IDX_DOCS','1', t, full_themes => FALSE, num_themes => 10);
DBMS_OUTPUT.PUT_LINE('themes='||t.COUNT);
END;36. THEMES: full themes в таблицу
CREATE TABLE ctx_themes (query_id NUMBER, theme VARCHAR2(2000), weight NUMBER);
BEGIN
CTX_DOC.THEMES('IDX_DOCS','1', 'CTX_THEMES', 11, full_themes => TRUE, num_themes => 20);
END;37. POLICY_THEMES: без индекса
DECLARE
t CTX_DOC.THEME_TAB;
txt CLOB := 'GPU and CPU performance for gaming computers';
BEGIN
CTX_DOC.POLICY_THEMES('P_TEXT', txt, t, full_themes => FALSE, num_themes => 5);
DBMS_OUTPUT.PUT_LINE('themes='||t.COUNT);
END;38. TOKENS: извлечь токены (in-memory)
DECLARE
tk CTX_DOC.TOKEN_TAB;
BEGIN
CTX_DOC.TOKENS('IDX_DOCS','2', tk);
DBMS_OUTPUT.PUT_LINE('tokens='||tk.COUNT);
END;39. TOKENS: с тезаурусом DEFAULT (synonyms)
DECLARE
tk CTX_DOC.TOKEN_TAB;
BEGIN
CTX_DOC.TOKENS('IDX_DOCS','1', tk, thes_name => 'DEFAULT', thes_toktype => 'SYN');
DBMS_OUTPUT.PUT_LINE('tokens='||tk.COUNT);
END;40. TOKENS: broader terms (BT) в таблицу
CREATE TABLE tokenstab (query_id NUMBER, token VARCHAR2(255), thes_tokens VARCHAR2(4000), offset NUMBER, length NUMBER);
BEGIN
CTX_DOC.TOKENS('IDX_DOCS','1', 'TOKENSTAB', 'DEFAULT', 'BT', 77);
END;41. GIST: параграфный краткий пересказ
DECLARE
out CLOB;
BEGIN
CTX_DOC.GIST('IDX_DOCS','1', out, glevel => 'P', pov => 'GENERIC', numParagraphs => 3);
DBMS_LOB.FREETEMPORARY(out);
END;42. GIST: тематический пересказ (POV)
DECLARE
out CLOB;
BEGIN
CTX_DOC.GIST('IDX_DOCS','1', out, glevel => 'S', pov => 'computers', numParagraphs => 6);
DBMS_LOB.FREETEMPORARY(out);
END;43. POLICY_LANGUAGES: определить язык текста
DECLARE
langs CTX_DOC.LANGUAGE_TAB;
txt CLOB := 'Этот документ на русском языке. This sentence is in English.';
BEGIN
CTX_DOC.POLICY_LANGUAGES('P_TEXT', txt, langs);
FOR i IN 1..langs.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(langs(i).language||' score='||langs(i).score);
END LOOP;
END;44. POLICY_STEMS: получить основы слов
DECLARE
stems CTX_DOC.STEM_GROUP_TAB;
txt CLOB := 'computers computing computed';
BEGIN
CTX_DOC.POLICY_STEMS('P_TEXT', txt, stems, language => 'ENGLISH');
DBMS_OUTPUT.PUT_LINE('stems groups='||stems.COUNT);
END;45. POLICY_NOUN_PHRASES: существительные словосочетания
DECLARE
np CTX_DOC.NOUN_PHRASE_TAB;
txt CLOB := 'The gaming laptop battery life and keyboard layout';
BEGIN
CTX_DOC.POLICY_NOUN_PHRASES('P_TEXT', txt, np, language => 'ENGLISH');
DBMS_OUTPUT.PUT_LINE('noun phrases='||np.COUNT);
END;46. POLICY_PART_OF_SPEECH: разметка частей речи
DECLARE
pos CTX_DOC.POS_TAB;
txt CLOB := 'Modern GPUs handle graphics efficiently';
BEGIN
CTX_DOC.POLICY_PART_OF_SPEECH('P_TEXT', txt, pos, language => 'ENGLISH');
DBMS_OUTPUT.PUT_LINE('pos tags='||pos.COUNT);
END;47. SENTIMENT: сегментный скоринг
DECLARE
seg CTX_DOC.SENTIMENT_SEGMENT_TAB;
txt CLOB := 'The pizza is great but delivery was slow';
BEGIN
CTX_DOC.SENTIMENT('P_TEXT', txt, seg);
DBMS_OUTPUT.PUT_LINE('segments='||seg.COUNT);
END;48. SENTIMENT_AGGREGATE: общий скоринг документа
DECLARE
agg NUMBER;
txt CLOB := 'I love the fresh salads and hate the long queue';
BEGIN
CTX_DOC.SENTIMENT_AGGREGATE('P_TEXT', txt, agg);
DBMS_OUTPUT.PUT_LINE('aggregate sentiment='||agg);
END;49. Лучшие практики: forward index и save copy
-- Для ускорения MARKUP/HIGHLIGHT/SNIPPET используйте forward index;
-- Для ускорения FILTER/GIST/THEMES/TOKENS — SAVE COPY (PLAINTEXT/FILTERED)
-- Пример уже показан в создании индекса: 'SAVE COPY FILTERED'.50. Очистка демо-объектов
BEGIN
CTX_DDL.DROP_POLICY('P_TEXT');
END;
DROP INDEX idx_docs;
DROP TABLE filtertab PURGE;
DROP TABLE filtertab2 PURGE;
DROP TABLE highlighttab PURGE;
DROP TABLE snippettab PURGE;
DROP TABLE tokenstab PURGE;
DROP TABLE ctx_themes PURGE;
DROP TABLE docs PURGE;Заключение
CTX_DOC закрывает большинство задач представления и анализа документов в Oracle Text:
фильтрация, разметка и подсветка, сниппеты, гисты, темы и токены, policy-варианты без индекса,
NLP-хелперы и сентимент-анализ. Подбирайте SAVE COPY и forward index под ваши нагрузки.
Ссылки на вендора
- Oracle — CTX_DOC Package (Reference, 23c)
- Oracle Text — Document Services, Forward Index & Save Copy
- Oracle — CTX_DDL (для CREATE_POLICY)