понедельник, 23 мая 2016 г.

Oracle Text: Высокоскоростной парсер поисковых запросов, часть III

Документация пакета CTX_API




Продолжение следует.



Oracle Text: Высокоскоростной парсер поисковых запросов, часть II

Введение

Окей, в предыдущей части вкратце была рассмотрена логика высокоскоростного парсера поисковых запросов в Oracle Text.

Следует отметить, что, хотя парсер в состоянии работать с контекстными индексами без дополнительных усложнений и наворотов, данный вариант поиска достаточно туповат и называть его в эпоху Гугла полнотекстовым можно лишь с большой натяжкой.

Это будет всего лишь дословный поиск по ключевым словам, что в большинстве случаев довольно-таки далеко от контекстного поиска.

Более того, это совсем не контекстный поиск. Как бы его не преподносили маркетологи компаний, использующих данный функционал. В нем нет ни расширения запросов по смыслу, ни поиска по темам (хотя это и возможно реализовать с использованием функций базисного функционала Oracle Text, в сравнении с поиском, например, Гугл, это всего лишь жалкое подобие левой руки).

Дело, собственно говоря, вот в чем. 

В Oracle Text есть единственная функция контекстного поиска. И это about()

Однако, проблема заключается в следующем.

Функция about() в отсутствие загруженных тезаурусов на соответствующих языках работает как простой поиск по ключевым словам. Причем:

  1. Тезаурусы по умолчанию не загружены
  2. В состав Oracle RDBMS включены считанные единицы тезаурусов на некоторых европейских языках
  3. Включенные в состав Oracle тезаурусы написаны индийскими студентами за еду и практически непригодны ввиду своего содержания для реального поиска
  4. Тезаурус русского языка отсутствует
  5. Тезаурусы Oracle должны иметь иерархическую структуру, что делает крайне сложным их разработку, сопровождение и применение в рамках сколько-нибудь сложного поискового функционала
Кроме того, в отсутствие тезаурусов принципиально сложно реализовать что-то сколько-нибудь похожее на функционал Гугла "Возможно, вы имели в виду....".

Данный парсер является попыткой решить часть этих задач:
  1. Попытаться все-таки добиться правильной работоспособности функции abount()
  2. Создать некий фреймворк для реализации гуглоподобного функционала "Возможно, вы имели в виду....", хотя и в несколько ином виде
  3. Создать некую обертку вокруг функционала Oracle Text по поддержке тезаурусов
  4. Разумеется, сохранить функциональность тупого дубового поиска "в лоб" по ключевым словам
  5. Создать некое подобие поискового языка запросов а-ля Гугл

Пакет ctx_api: парсер и функционал тезауруса

Итак, вот полный код пакета.

Спецификация ctx_api.sql:
 -------------------------------------------------------------------------------  
 -- PROJECT_NAME: CTX_API                           --  
 -- RELEASE_VERSION: 1.0.0.5                         --  
 -- RELEASE_STATUS: Release                          --  
 --                                      --  
 -- REQUIRED_ORACLE_VERSION: 10.1.0.x                     --  
 -- MINIMUM_ORACLE_VERSION: 9.2.0.3                      --  
 -- MAXIMUM_ORACLE_VERSION: 11.x.x.x                     --  
 -- PLATFORM_IDENTIFICATION: Generic                     --  
 --                                      --  
 -- IDENTIFICATION: ctx_api.sql                        --  
 -- DESCRIPTION: CTX_API package. Contains ConText Search and thesaurus API. --  
 -- ------------------------------------------------------------------------- --  
 -- Package table of contents, syntax and components descriptions:      --  
 -- ------------------------------------------------------------------------- --  
 -- function version return varchar2 deterministic;              --  
 --                                      --  
 -- Function returns hardcoded API version for application alternative calls. --  
 -- No arguments.                               --  
 -- ------------------------------------------------------------------------- --  
 -- function phrase_exists (p_phrase in varchar2,               --  
 --             p_thes_name in varchar2              --  
 --             default 'default') return boolean         --  
 --             deterministic;                  --   
 --                                      --  
 -- Function checks phrase exists in specified thesaurus.           --  
 --                                      --   
 -- Arguments:                                --  
 -- p_phrase - specified term. Can be phrase and can contain qualifier.    --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- function phrase_relation_exists (p_phrase in varchar2,          --  
 --                 p_relation in varchar2          --  
 --                 default 'bt,btp,nt,ntp,rt,syn',     --  
 --                 p_thes_name in varchar2         --  
 --                 default 'default') return boolean    --  
 --                 deterministic;              --  
 --                                      --  
 -- Function checks relations in thesaurus for specified term.        --  
 --                                      --  
 -- Arguments:                                --  
 -- p_phrase - specified term. Can be phrase and can contain qualifier.    --  
 -- p_relation - relations list. Can contain one or more standard relation  --  
 --       keywords with comma-separated list.             --  
 --       Examples: 'bt,ntg','syn','rt','bt,btp,ntp' etc.       --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- function search_expansion_level (p_phrase in varchar2,          --  
 --                 p_thes_name in varchar2         --  
 --                 default 'default') return number     --  
 --                 deterministic;              --  
 --                                      --  
 -- Function returns BT/NT relations expansion level, which contains more   --  
 -- than c_nt_terms NT's for phrase subtree.                 --  
 --                                      --  
 -- Arguments:                                --  
 -- p_phrase - specified term. Can be phrase and can contain qualifier.    --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- Known issues: Function returns 0 if phrase is Top Term or absent in thes. --  
 -- ------------------------------------------------------------------------- --  
 -- function search_expansion_term (p_phrase in varchar2,           --  
 --                 p_thes_name in varchar2          --  
 --                 default 'default') return varchar2    --  
 --                 deterministic;              --  
 --                                      --  
 -- Function returns BT subcategory for given phrase subtree, for which BT  --  
 -- contains more than c_nt_terms NT's.                    --  
 --                                      --  
 -- Arguments:                                --  
 -- p_phrase - specified term. Can be phrase and can contain qualifier.    --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 --                                      --  
 -- Known issues: Function returns 0 if phrase is Top Term or absent in thes. --  
 -- ------------------------------------------------------------------------- --  
 -- function has_homographs (p_phrase in varchar2,              --   
 --             p_thes_name in varchar2             --  
 --             default 'default') return boolean        --  
 --             deterministic;                  --  
 --                                      --  
 -- Function checks therm for homographs. If homographs exists, returns true. --  
 --                                      --  
 -- Arguments:                                --  
 -- p_phrase - term for checking.                       --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- procedure get_qualifiers ( p_qualifiers out ctx_api.term_tab,       --  
 --              p_phrase in varchar2,             --  
 --              p_thes_name in varchar2 default 'default');  --  
 --                                      --  
 -- Procedure returns term qualifiers if they exists. If term not exists in  --  
 -- thesaurus, exception ORA-20151 raised.                  --  
 --                                      --  
 -- Arguments:                                --  
 -- p_qualifiers - return structure (table of varchar2) by ctx_api.term_tab. --  
 -- p_phrase - term for checking.                       --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 --                                      --  
 -- Known issues: If term is only one (has no homograps), but defined with  --  
 -- qualifier, procedure returns this qualifier anyway.            --  
 -- ------------------------------------------------------------------------- --  
 -- function get_note (p_phrase in varchar2,                 --  
 --          p_thes_name in varchar2                --  
 --          default 'default') return varchar2           --  
 --          deterministic;                     --  
 --                                      --  
 -- Get scope note (SN) for phrase if it exists. Otherwise function returns  --  
 -- empty string.                               --  
 --                                      --  
 -- Arguments:                                --  
 -- p_phrase - specified term. Can be phrase and can contain qualifier.    --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- function get_bt (p_phrase in varchar2,                  --  
 --         p_level in number default 1,               --  
 --         p_thes_name in varchar2 default 'default')        --  
 --         return varchar2 deterministic;              --   
 --                                      --  
 -- Function returns single BT for term.                   --  
 -- If term not exists in thesaurus, then returns themself.          --  
 -- If term has homographs, but no qualifiers specified, returns exception  --  
 -- ORA-20152.                                --   
 -- If term has homographs, and qualifier specified, returns BT term with   --  
 -- specified level (p_level).                        --  
 --                                      --  
 -- Arguments:                                --  
 -- p_phrase - term or phrase for BT.                     --  
 -- p_level - expansion level for BT.                     --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- Known issues: If term has only BTP hierarchical relation, it threats as  --  
 --        BT relation.                        --  
 -- ------------------------------------------------------------------------- --  
 -- procedure get_bt (p_bt out ctx_api.term_tab,               --  
 --          p_phrase in varchar2,                 --  
 --          p_level in number default 1,              --  
 --          p_thes_name in varchar2 default 'default');      --  
 --                                      --  
 -- Procedure returns all BT's for term (BT subtree).             --  
 -- If term not exists in thesaurus, then returns themself.          --  
 -- If term has homographs, but no qualifiers specified, returns all BT's   --  
 -- subtrees, one by one, each starting with given term with qualifier.    --  
 -- If term has homographs and qualifier specified, procedure returns only it --  
 -- BT's subtree.                               --  
 -- Note: Output table contains BT's sorted by level in descending order.   --  
 -- Example:                                 --  
 --    cat    - lowest level                      --  
 --    animals  - level up                        --  
 --    zoology  - level up                        --  
 --    science  - level up                        --  
 --                                      --  
 -- Arguments:                                --  
 -- p_bt - return structure (table of varchar2) by ctx_api.term_tab.     --  
 -- p_phrase - term or phrase for BT.                     --  
 -- p_level - expansion level for BT. Restricts expansion level.       --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- Known issues: If term has only BTP hierarchical relation, it threats as  --  
 --        BT relation.                        --  
 -- ------------------------------------------------------------------------- --  
 -- procedure get_nt (p_nt out ctx_api.term_tab,               --  
 --          p_phrase in varchar2,                  --  
 --          p_level in number default 1,              --  
 --          p_thes_name in varchar2 default 'default');       --  
 --                                      --  
 -- Procedure returns NT's for term.                     --  
 -- If term not exists in thesaurus, returns exception ORA-20151.       --  
 -- If term has homographs, but no qualifiers specified, returns exception  --  
 -- ORA-20152.                                --  
 -- If term has homographs, and qualifier specified, returns NT's for term  --  
 -- with specified level (p_level).                      --  
 -- If term is lowest level (has no NT's), then return themself.       --  
 --                                      --  
 -- Arguments:                                --  
 -- p_nt - return structure (table of varchar2) by ctx_api.term_tab.     --  
 -- p_phrase - term or phrase for BT.                     --  
 -- p_level - expansion level for BT.                     --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- procedure get_ntp (p_ntp out ctx_api.term_tab,              --  
 --          p_phrase in varchar2,                 --  
 --          p_level in number default 1,              --  
 --          p_thes_name in varchar2 default 'default');      --  
 --                                      --  
 -- Procedure returns NTP's for term.                     --  
 -- If term not exists in thesaurus, returns exception ORA-20151.       --  
 -- If term has homographs, but no qualifiers specified, returns exception.  --  
 -- If term has homographs, and qualifier specified, returns NTP's for term  --  
 -- with specified level (p_level).                      --  
 -- If term is lowest level (has no NTP's), then return themself.       --  
 --                                      --  
 -- Arguments:                                --  
 -- p_ntp - return structure (table of varchar2) by ctx_api.term_tab.     --  
 -- p_phrase - term or phrase for BT.                     --  
 -- p_level - expansion level for BT.                     --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- procedure get_rt (p_rt out ctx_api.term_tab,               --  
 --         p_phrase in varchar2,                  --  
 --         p_thes_name in varchar2 default 'default');       --  
 --                                      --  
 -- Procedure returns RT's for term.                     --  
 -- If term not exists in thesaurus, returns exception ORA-20151.       --  
 -- If term has homographs, but no qualifiers specified, returns exception  --  
 -- ORA-20152.                                --  
 -- If term has homographs, and qualifier specified, returns RT's for term.  --  
 -- If term has no RT's, then return themself.                --  
 --                                      --  
 -- Arguments:                                --  
 -- p_rt - return structure (table of varchar2) by ctx_api.term_tab.     --  
 -- p_phrase - term or phrase for RT.                     --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- procedure get_syn (p_syn out ctx_api.term_tab,              --  
 --          p_phrase in varchar2,                  --  
 --          p_thes_name in varchar2 default 'default');       --  
 --                                      --  
 -- Procedure returns SYN's for term.                     --  
 -- If term not exists in thesaurus, returns exception ORA-20151.       --  
 -- If term has homographs, but no qualifiers specified, returns exception.  --  
 -- If term has homographs, and qualifier specified, returns SYN's for term. --  
 -- If term has no SYN's, then return themself.                --  
 --                                      --  
 -- Arguments:                                --  
 -- p_syn - return structure (table of varchar2) by ctx_api.term_tab.     --  
 -- p_phrase - term or phrase for SYN.                    --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- ------------------------------------------------------------------------- --  
 -- function search_string_parser (p_search_str in varchar2,         --  
 --                p_query_mode in varchar2 default 'keyword',--  
 --                p_logical_op in varchar2 default 'and',  --  
 --                p_query_opt in varchar2          --  
 --                default ctx_api.c_query_op_about,     --  
 --                p_expansion_level in number default 1,   --  
 --                p_thes_name in varchar2 default 'default', --  
 --                p_refine_on in number           --  
 --                default ctx_api.c_refine_off,       --  
 --                p_exp_detail_on in number         --  
 --                default ctx_api.c_exp_detail_off)     --  
 --                return varchar2 deterministic;       --  
 --                                      --  
 -- Function is universal parser, supports multiply thesauruses and main   --  
 -- ISO-2788 and ANSI Z39.19 relations. Also supports all logical operands  --  
 -- and composed phrases and qualifiers.                   --  
 --                                      --  
 -- Arguments:                                --  
 -- p_search_string - incoming search string in varchar2 single-byte charset, --  
 --          string length must not be greater than 4000 chars.   --  
 --          When parameter value is null, function returns null.  --  
 -- p_query_mode - parameter controls parsing logic for two different modes: --  
 --        - KEYWORD search (thesaurus functions not uses);      --  
 --        - CONCEPT search (thesaurus functions uses);        --  
 --        The parameters values are: 'keyword' or 'concept'.     --  
 --        Default value is 'keyword' (no thesaurus loaded or cannot --  
 --        identify query mode from parameter).            --  
 -- p_logical_op - controls how tokens will be joined in return string.    --  
 --        Having two values: and, or. Anyway, this is logical    --  
 --        operand for search tokens. Default value is AND.      --  
 -- p_query_opt - controls using thesaurus function in CONCEPT mode. Having  --  
 --        five possible values:                    --  
 --        about, nt, bt, rt or syn. Default value is 'about'.     --  
 --        In keyword mode will be ommitted.              --   
 -- p_expansion_level - expansion hierarchical functions (relations) level.  --  
 --           Will be used only with nt and bt relations in concept --  
 --           mode. In keyword mode and with syn, rt and about will --  
 --           be ignored. If specified default value or 1,then will --  
 --           be ommitted and default level 1 will be used.     --  
 -- p_thes_name - thesaurus name in concept mode. Default value is 'default'. --  
 --        In keyword mode, with about and if value is 'default', it  --  
 --        will be ommitted. Value can be <= 30 characters.      --   
 -- p_refine_on - Refine search flag.                     --   
 --      If 0 (default), query will not be refined.           --  
 --      If 1, context refiner subfunction is ON (only in CONCEPT query --  
 --      mode, when using thesaurus) and any words not satisfied main  --  
 --      query context will be dropped from result string.       --  
 --      See documentation to learn how context refiner function works. --  
 -- p_exp_detail_on - Expand NT/BT queries category level. If enabled,    --  
 --          expansion level is set to subtree category, in which  --  
 --          NT's more than c_nt_terms constant.           --  
 -- NOTE: If this flag enabled, p_expansion_level WILL BE IGNORED.      --  
 -- ------------------------------------------------------------------------- --  
 -- Function parses search string using this RULES:              --  
 -- 1. All punctuation removes from string.                  --  
 -- 2. All garbage symbols (which can generate problems with ConText) removes.--  
 -- 3. String disassembles to the tokens, analyzes and assembles back.    --  
 -- 4. Before assembling, logical operators and thesaurus functions adds to  --  
 --  tokens.                                --  
 -- 5. When analyzes, some syntax uses as controls:              --  
 --  - Words wrapped into "" threats as single token;            --    
 --  - Words having thesaurus qualifier (i.e. cat (animals) ) threats as  --  
 --   single token, qualifier remains;                   --  
 --  - Words concatenated with qualifier (i.e. cat(animals) ) also threats --  
 --   as single token, qualifier remains;                 --  
 --  - Empty brackets removes from string (threats as no token);      --  
 --  - When "" not even in search string, last " will be ignored.      --  
 -- 6. All 'unsafe' constructions wrappes into escape symbols ({}).      --  
 -- 7. ConText reserved words removes from search string, or escapes depends --  
 --  p_query_mode value.                          --   
 -- 8. If metasymbols '%' or '_' found in search string, they pass through in --  
 --  output string in KEYWORD mode, and will be removed in CONCEPT mode.  --  
 -- ------------------------------------------------------------------------- --                                     --  
 -- Usage example:                              --  
 -- SQL> declare                               --  
 -- 2  v_str varchar2(50) := 'кот пес котопес (животные)';         --  
 -- 3  v_out varchar2(50);                         --  
 -- 4 begin                                 --  
 -- 5  v_out := ctx_api.search_string_parser(v_str,'concept','and','nt');  --  
 -- 6  dbms_output.put_line(v_out);                     --  
 -- 7 end;                                 --  
 -- 8 /                                   --  
 -- nt({кот}) and nt({пес}) and nt({котопес})                 --  
 --                                      --  
 -- PL/SQL procedure successfully completed.                 --  
 -- ------------------------------------------------------------------------- --  
 -- function term_counter (p_thes_name in varchar2 default 'default')     --  
 --            return number;                   --  
 --                                      --  
 -- Function counts distinct terms in specified thesaurus.          --  
 --                                      --  
 -- Arguments:                                --  
 -- p_thes_name - thesaurus name. Default value is 'default'.         --  
 -- If thesauri not found, returns exception ORA-20150.            --  
 -- ------------------------------------------------------------------------- --  
 -- procedure thes_loaded (p_ths_list out ctx_api.thes_tab);         --  
 --                                      --  
 -- Procedure returns loaded thesauri list. If no thesauri loaded, returns  --  
 -- exception ORA-20154.                           --  
 --                                      --  
 -- Arguments:                                --  
 -- p_ths_list has type ctx_api.thes_tab which is thesauri name list of    --  
 -- varchar2(30).                               --  
 -- ------------------------------------------------------------------------- --  
 -- Known issues:                               --  
 -- 1. p_thes_name cannot contain "_" symbols,                --   
 --  it will be removed from result string!                 --  
 -- 2. If p_exp_detail_on is enabled,                     --  
 --  p_expansion_level value will be ignored.                --  
 -- 3. If p_query_opt in ('about','syn','rt') or               --  
 --  p_query_mode = 'keyword',                       --  
 --  p_exp_detail_on will be ignored.                    --  
 -- ------------------------------------------------------------------------- --  
 --                                      --  
 -- INTERNAL_FILE_VERSION: 0.0.1.3                      --  
 --                                      --  
 -- COPYRIGHT: Yuri Voinov (C) 2004, 2016                   --  
 --                                      --  
 -- MODIFICATIONS:                              --  
 -- 23.05.2016 -Update copyright.                       --  
 -- 29.03.2008 -Functions phrase_exists and get_note added.          --  
 -- 10.08.2007 -Thesaurus content API added.                 --  
 -- 28.12.2006 -Added overloaded proc get_bt for returning subtrees of BT's. --  
 -- 19.12.2006 -Fix major bug in has_homographs function. Add get_rt, get_syn --  
 --       functions.                          --  
 -- 09.12.2006 -Rename all package constants.                 --  
 -- 03.12.2006 -Type qual_tab and get_qualifiers procedure added. version and --  
 --       has_homographs functions added.                --  
 -- 18.11.2006 -Query options constants added. Expansion level definer const --  
 --       and related functions added. Relation exists function added. --  
 -- 18.06.2006 -Refiner constants added.                   --  
 -- 12.06.2006 -Refiner function added. Free memory call optimized. Exception --  
 --       TEXT_ERROR added (ORA-20150), also when no thesaurus.     --   
 -- 01.03.2006 -p_expansion_level and p_thes_name parameters added.      --  
 -- 18.02.2006 -Initial code written.                     --  
 -------------------------------------------------------------------------------  
   
 create or replace package ctx_api authid current_user is  
   
  -- API version  
  function version return varchar2 deterministic;  
   
  -- Thesaurus CTX API  
  -- Package constants  
  c_query_op_about constant varchar2(5) := 'about'; -- ABOUT query option  
  c_query_op_bt constant varchar2(2) := 'bt'; -- BT query option  
  c_query_op_nt constant varchar2(2) := 'nt'; -- NT query option  
  c_query_op_rt constant varchar2(2) := 'rt'; -- RT query option  
  c_query_op_syn constant varchar2(3) := 'syn'; -- SYN query option  
   
  c_refine_on constant number(1) := 1; -- Context refiner ON  
  c_refine_off constant number(1) := 0; -- Context refiner OFF  
   
  c_exp_detail_on constant number(1) := 1; -- Context expansion ON  
  c_exp_detail_off constant number(1) := 0; -- Context expansion OFF  
   
  c_nt_terms constant number(2) := 5; -- Expansion level stop quantity.  
                    -- Stop expansion level if NT's  
                    -- in subtree more than that constant.   
  -- CTX API types  
  type term_tab is table of varchar2(256) index by binary_integer;  
   
  -- Check phrase exists in specified thesaurus  
  function phrase_exists (p_phrase in varchar2,  
              p_thes_name in varchar2   
              default 'default') return boolean  
              deterministic;  
   
  -- Check relation exists  
  function phrase_relation_exists (p_phrase in varchar2,  
                  p_relation in varchar2  
                  default 'bt,btp,nt,ntp,rt,syn',  
                  p_thes_name in varchar2   
                  default 'default') return boolean  
                  deterministic;  
   
  -- NT/BT Expansion level definer  
  function search_expansion_level (p_phrase in varchar2,  
                  p_thes_name in varchar2   
                  default 'default') return number  
                  deterministic;  
   
  -- NT/BT Expansion level term  
  function search_expansion_term (p_phrase in varchar2,  
                  p_thes_name in varchar2   
                  default 'default') return varchar2  
                  deterministic;  
   
  -- Check term homographs  
  function has_homographs (p_phrase in varchar2,  
              p_thes_name in varchar2   
              default 'default') return boolean  
              deterministic;  
   
  -- Get homograph's term qualifiers  
  procedure get_qualifiers (p_qualifiers out ctx_api.term_tab,  
               p_phrase in varchar2,  
               p_thes_name in varchar2 default 'default');  
   
  -- Get scope note (SN) for phrase if it exists.  
  -- Otherwise returns empty string.  
  function get_note (p_phrase in varchar2,  
           p_thes_name in varchar2   
           default 'default') return varchar2  
           deterministic;  
   
  -- Get term BT  
  function get_bt (p_phrase in varchar2,  
          p_level in number default 1,  
          p_thes_name in varchar2 default 'default')   
          return varchar2 deterministic;  
   
  -- Get all term BT's  
  procedure get_bt (p_bt out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_level in number default 1,  
           p_thes_name in varchar2 default 'default');  
   
  -- Get term NT's  
  procedure get_nt (p_nt out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_level in number default 1,  
           p_thes_name in varchar2 default 'default');  
   
  -- Get term NTP's  
  procedure get_ntp (p_ntp out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_level in number default 1,  
           p_thes_name in varchar2 default 'default');  
   
  -- Get term RT's  
  procedure get_rt (p_rt out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_thes_name in varchar2 default 'default');  
   
  -- Get term SYN's  
  procedure get_syn (p_syn out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_thes_name in varchar2 default 'default');  
   
  -- Universal parser   
  function search_string_parser (p_search_str in varchar2,  
                 p_query_mode in varchar2 default 'keyword',  
                 p_logical_op in varchar2 default 'and',  
                 p_query_opt in varchar2   
                 default ctx_api.c_query_op_about,  
                 p_expansion_level in number default 1,  
                 p_thes_name in varchar2 default 'default',  
                 p_refine_on in number   
                 default ctx_api.c_refine_off,  
                 p_exp_detail_on in number  
                 default ctx_api.c_exp_detail_off)   
                 return varchar2 deterministic;  
   
  -- Thesaurus content API  
  type thes_tab is table of varchar2(30) index by binary_integer;  
   
  -- Specified thesaurus term counter  
  function term_counter (p_thes_name in varchar2 default 'default')  
             return number deterministic;  
   
  -- Get loaded thesauruses  
  procedure thes_loaded (p_ths_list out ctx_api.thes_tab);  
   
  pragma restrict_references (default, wnds, wnps, rnds, rnps, trust);   
   
 end ctx_api;  
 /  
   
 show errors  
   
 -- Can be security issue.  
 -- Revoke exec privilege from public and grant  
 -- to only few users if you need.  
 -- Grant to public need only if package routines  
 -- calls from Web (modplsql).  
 grant execute on ctx_api to public  
 /  
   

В комментариях в принципе все задокументировано.

И - тело пакета prvtctxapi.pls:

 --------------------------------------------  
 --  PRIVATE IMPLEMENTATION (INTERNALS)  --  
 --    Yuri Voinov (C) 2004, 2016   --  
 --   MUST BE WRAPPED BEFORE LOAD !   --  
 --   Do not distribute this code !   --  
 --------------------------------------------  
 create or replace package body ctx_api is  
  -- --------------------------------------------------------------  
  type ReservedWordList is table of varchar2(8);  
  v_reserved ReservedWordList := ReservedWordList('ABOUT','ACCUM','BT','BTG','BTI','BTP','FUZZY','HASPATH',  
                  'INPATH','MDATA','MINUS','NEAR','NT','NTG','NTI','NTP','PT','RT','SQE',  
                  'SYN','TR','TRSYN','TT','WITHIN');  
   
  c_version constant varchar2(30) := '1.0.0.5'; -- API version  
   
  v_restab ctx_thes.exp_tab; -- BT's result table  
  v_restab2 ctx_thes.exp_tab; -- NT's result table  
   
  v_qualifier varchar2(256); -- Qualifier buffer  
  v_phrase varchar2(256);  -- Global phrase buffer  
   
  text_error exception; -- Uses for specified thesaurus exists check  
  term_has_homographs exception; -- Exception if term or phrase has homographs  
  pragma exception_init(text_error, -20000); -- Raised when specified thesaurus not loaded  
  phrase_error exception;  
  pragma exception_init(phrase_error, -20151); -- Phrase not found or thes not loaded.  
  -- --------------------------------------------------------------  
  procedure free_exp_memory is  
  /* INTERNAL USE */  
  /* Uses for clean out PL/SQL tables in expansion functions */  
  begin  
  v_restab.delete;  
  v_restab2.delete;  
  end free_exp_memory;  
  -- --------------------------------------------------------------  
  -- Return hardcoded API version  
  function version return varchar2 deterministic is  
  begin  
  return c_version;  
  end version;  
  -- --------------------------------------------------------------  
  function phrase_exists (p_phrase in varchar2,  
              p_thes_name in varchar2   
              default 'default') return boolean  
              deterministic is  
  v_phrase varchar2(512);   -- Phrase refine buffer  
  v_phrase_get varchar2(256); -- Get phrase buffer  
  begin  
  -- Refine p_phrase if contain qualifier  
  if instr(p_phrase,'(') > 0 and instr(p_phrase,')') > 0 then  
   v_phrase := trim(substr(p_phrase, 1, instr(p_phrase,'(')-1));  
  else  
   v_phrase := p_phrase;  
  end if;  
   
  select thp_phrase  
  into v_phrase_get  
  from ctx_thes_phrases  
  where thp_thesaurus = upper(p_thes_name)  
   and thp_phrase = upper(v_phrase);  
   
  return true;  
  exception  
  when too_many_rows then return true;  
  when no_data_found then return false;  
  when others then  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  end phrase_exists;  
  -- --------------------------------------------------------------  
  function phrase_relation_exists (p_phrase in varchar2,  
                  p_relation in varchar2  
                  default 'bt,btp,nt,ntp,rt,syn',  
                  p_thes_name in varchar2 default 'default')   
                  return boolean deterministic is  
  v_result boolean;  
  v_relation varchar2(50);  
  begin  
  v_relation := upper(p_relation);  
  return ctx_thes.has_relation(p_phrase, v_relation, p_thes_name);  
  exception  
  when text_error then  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  when others then  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end phrase_relation_exists;  
  -- --------------------------------------------------------------  
  function search_expansion_level (p_phrase in varchar2,  
                  p_thes_name in varchar2 default 'default')   
                  return number deterministic is  
  v_init_level number := 0; -- Initial expansion level  
  v_top_bt varchar2(255);  -- BT term buffer  
  v_nts pls_integer;    -- NT's counter  
  begin  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  loop  
   v_init_level := v_init_level + 1; -- Increment level  
   ctx_thes.bt(v_restab, v_phrase, v_init_level, p_thes_name); -- Get top BT for current level  
   v_top_bt := v_restab(v_restab.last).xphrase; -- Get category for current level  
   ctx_thes.nt(v_restab2, v_top_bt, v_init_level, p_thes_name); -- Check NT's  
   v_nts := v_restab2.count; -- Count NT's for current BT level  
  exit when v_nts >= ctx_api.c_nt_terms or v_restab(v_restab.last).xlevel = 0;  
  end loop;  
  return v_restab(v_restab.last).xlevel; -- Return xlevel  
  free_exp_memory; -- Clean out PL/SQL tables  
  exception  
  when text_error then  
   free_exp_memory; -- Clean out PL/SQL tables  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  when others then  
   free_exp_memory; -- Clean out PL/SQL tables  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end search_expansion_level;  
  -- --------------------------------------------------------------  
  function search_expansion_term (p_phrase in varchar2,  
                  p_thes_name in varchar2 default 'default')   
                  return varchar2 deterministic is  
  v_init_level number := 0; -- Initial expansion level  
  v_top_bt varchar2(255); -- BT term buffer  
  v_nts pls_integer;    -- NT's counter  
  begin  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  loop  
   v_init_level := v_init_level + 1; -- Increment level  
   ctx_thes.bt(v_restab, v_phrase, v_init_level, p_thes_name); -- Get top BT for current level  
   v_top_bt := v_restab(v_restab.last).xphrase; -- Get category for current level  
   ctx_thes.nt(v_restab2, v_top_bt, v_init_level, p_thes_name); -- Check NT's  
   v_nts := v_restab2.count; -- Count NT's for current BT level  
  exit when v_nts >= ctx_api.c_nt_terms or v_restab(v_restab.last).xlevel = 0;  
  end loop;  
  return v_top_bt; -- Return BT term  
  free_exp_memory; -- Clean out PL/SQL tables  
  exception  
  when text_error then  
   free_exp_memory; -- Clean out PL/SQL tables  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  when others then  
   free_exp_memory; -- Clean out PL/SQL tables  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end search_expansion_term;  
  -- --------------------------------------------------------------  
  function has_homographs (p_phrase in varchar2,  
              p_thes_name in varchar2 default 'default')   
              return boolean deterministic is  
  v_phrase_tmp varchar2(256);  
  v_qualifier varchar2(256);  
  begin  
  if instr(p_phrase,'(') = 0 and instr(p_phrase,')') = 0 then  
   -- If p_phrase not contains qualifiers, get it as is.  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  else  
   -- If p_phrase contains qualifiers, lets remove qualifier  
   -- from open "(" to the end of line.  
   v_phrase := substr(trim(p_phrase), 1, instr(trim(p_phrase),'(') - 2);  
  end if;  
  select ctxsys.ctx_thes_phrases.thp_phrase,  
      ctxsys.ctx_thes_phrases.thp_qualifier -- Get phrase and qualifier from thesauri  
  into v_phrase_tmp, v_qualifier  
  from ctxsys.ctx_thes_phrases -- View ctx_thes_phrase in schema CTXSYS  
  where ctxsys.ctx_thes_phrases.thp_thesaurus = upper(p_thes_name)  
   and ctxsys.ctx_thes_phrases.thp_phrase = upper(v_phrase);  
  if v_phrase_tmp is not null or v_qualifier is null   
   then return false; -- Return false if term only one or has no qualifiers  
  end if; -- (but if no qualifiers and term more than once, raises exception)  
  exception  
  when too_many_rows then  
   return true; -- If select returns more than one rows, homographs exists  
  when no_data_found then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end has_homographs;  
  -- --------------------------------------------------------------  
  function get_note (p_phrase in varchar2,  
           p_thes_name in varchar2   
           default 'default') return varchar2  
           deterministic is  
  -- Get scope note (SN) for phrase if SN exists.  
  -- Otherwise returns empty string.  
  v_phrase varchar2(512);   -- Phrase buffer  
  v_qualifier varchar2(256); -- Qualifier buffer  
  v_note varchar2(2000) := '';  
  phrase_not_exists exception;  
  begin  
  -- Check phrase existence  
  if not ctx_api.phrase_exists(p_phrase, p_thes_name) then  
   raise phrase_not_exists;  
  end if;  
  -- Refine p_phrase if contain qualifier  
  if instr(p_phrase,'(') > 0 and instr(p_phrase,')') > 0 then  
   v_phrase := trim(substr(p_phrase, 1, instr(p_phrase,'(')-1));  
   v_qualifier := trim(substr(p_phrase, instr(p_phrase,'('), instr(p_phrase,')')-1));  
   -- Selet note for phrase with qualifier  
   select thp_scope_note  
   into v_note  
   from ctx_thes_phrases  
   where thp_thesaurus = upper(p_thes_name)  
    and thp_phrase = upper(v_phrase)  
    and (thp_qualifier is not null   
      and thp_qualifier = upper(v_qualifier));  
  else  
   v_phrase := p_phrase;  
   -- Selet note for phrase without qualifier  
   select thp_scope_note  
   into v_note  
   from ctx_thes_phrases  
   where thp_thesaurus = upper(p_thes_name)  
    and thp_phrase = upper(v_phrase);  
  end if;  
  return v_note; -- Return SN  
  exception  
  -- Catch both exceptions. Function produces too_many_rows  
  -- exception on some thesauri data.  
  when no_data_found or too_many_rows then return null;  
  when phrase_not_exists then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  end get_note;  
  -- --------------------------------------------------------------  
  procedure get_qualifiers (p_qualifiers out ctx_api.term_tab,  
               p_phrase in varchar2,  
               p_thes_name in varchar2 default 'default') is  
  v_qualifiers ctx_api.term_tab;  
  not_found exception;  
  begin  
  if instr(p_phrase,'(') = 0 and instr(p_phrase,')') = 0 then  
   -- If p_phrase not contains qualifiers, get it as is.  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  else  
   -- If p_phrase contains qualifiers, lets remove qualifier  
   -- from open "(" to the end of line.  
   v_phrase := substr(trim(p_phrase), 1, instr(trim(p_phrase),'(') - 2);  
  end if;  
  select ctxsys.ctx_thes_phrases.thp_qualifier -- Bulk collect qualifiers for term p_phrase  
  bulk collect into v_qualifiers  
  from ctxsys.ctx_thes_phrases  
  where ctxsys.ctx_thes_phrases.thp_thesaurus = upper(p_thes_name)  
   and ctxsys.ctx_thes_phrases.thp_phrase = upper(v_phrase);  
  if sql%notfound then raise not_found; end if; -- If phrase not found, raise exception  
  p_qualifiers := v_qualifiers; -- Return qualifiers  
  v_qualifiers.delete;     -- Free memory  
  exception  
  when not_found then  
   v_qualifiers.delete; -- Free memory  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_qualifiers.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_qualifiers;  
  -- --------------------------------------------------------------  
  function get_bt (p_phrase in varchar2,  
          p_level in number default 1,  
          p_thes_name in varchar2 default 'default')   
          return varchar2 deterministic is  
  v_res ctx_thes.exp_tab; -- ctx_thes.bt result buffer  
  v_ret varchar2(256); -- Return buffer  
  begin -- Check phrase for homographs if qualifier is absent  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  if instr(v_phrase,'(') = 0 and instr(v_phrase,')') = 0 then  
   if ctx_api.has_homographs (v_phrase, p_thes_name) then  
   raise term_has_homographs;  
   end if;  
  end if;  
  ctx_thes.bt(v_res, v_phrase, p_level, p_thes_name); -- Get BT's level p_level  
  for i in v_res.last..v_res.last -- Extract only LAST BT  
  loop  
   v_ret := v_res(i).xphrase;  
  end loop;  
  return v_ret; -- Return BT  
  v_res.delete; -- Free memory  
  exception   
  when term_has_homographs then  
   raise_application_error(-20152,'Phrase "'||upper(p_phrase)||'" has homographs.');  
  when phrase_error then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_res.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_bt;  
  -- --------------------------------------------------------------  
  procedure get_bt (p_bt out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_level in number default 1,  
           p_thes_name in varchar2 default 'default') is  
  v_res ctx_thes.exp_tab; -- ctx_thes.bt result buffer  
  begin  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  ctx_thes.bt(v_res, v_phrase, p_level, p_thes_name); -- Get BT's level p_level  
  if v_res.count = 1 then p_bt(1) := v_res(1).xphrase; -- If BT's absent, return phrase.  
  else -- If phrase has BT's, then return them all, exclude top - original phrase  
   for i in 2..v_res.last -- Extract only BT's  
   loop  
   p_bt(i-1) := v_res(i).xphrase; -- Let's count return array from 1  
   end loop;  
  end if;  
  v_res.delete; -- Free memory  
  exception   
  when phrase_error then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_res.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_bt;  
  -- --------------------------------------------------------------  
  procedure get_nt (p_nt out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_level in number default 1,  
           p_thes_name in varchar2 default 'default') is  
  v_res ctx_thes.exp_tab; -- ctx_thes.nt result buffer  
  begin -- Check phrase for homographs if qualifier is absent  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  if instr(v_phrase,'(') = 0 and instr(v_phrase,')') = 0 then  
   if ctx_api.has_homographs (v_phrase, p_thes_name) then  
   raise term_has_homographs;  
   end if;  
  end if;  
  ctx_thes.nt(v_res, v_phrase, p_level, p_thes_name); -- Get NT's level p_level  
  if v_res.count = 1 then p_nt(1) := v_res(1).xphrase; -- If NT's absent, return phrase.  
  else -- If phrase has NT's, then return them all, exclude top - original phrase  
   for i in 2..v_res.last -- Extract only NT's  
   loop  
   p_nt(i-1) := v_res(i).xphrase; -- Let's count return array from 1  
   end loop;  
  end if;  
  v_res.delete; -- Free memory  
  exception   
  when term_has_homographs then  
   raise_application_error(-20152,'Phrase "'||upper(p_phrase)||'" has homographs.');  
  when phrase_error then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_res.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_nt;  
  -- --------------------------------------------------------------  
  procedure get_ntp (p_ntp out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_level in number default 1,  
           p_thes_name in varchar2 default 'default') is  
  v_res ctx_thes.exp_tab; -- ctx_thes.ntp result buffer  
  begin -- Check phrase for homographs if qualifier is absent  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  if instr(v_phrase,'(') = 0 and instr(v_phrase,')') = 0 then  
   if ctx_api.has_homographs (v_phrase, p_thes_name) then  
   raise term_has_homographs;  
   end if;  
  end if;  
  ctx_thes.ntp(v_res, v_phrase, p_level, p_thes_name); -- Get NTP's level p_level  
  if v_res.count = 1 then p_ntp(1) := v_res(1).xphrase; -- If NTP's absent, return phrase.  
  else -- If phrase has NTP's, then return them all, exclude top - original phrase  
   for i in 2..v_res.last -- Extract only NTP's  
   loop  
   p_ntp(i-1) := v_res(i).xphrase; -- Let's count return array from 1  
   end loop;  
  end if;  
  v_res.delete; -- Free memory  
  exception   
  when term_has_homographs then  
   raise_application_error(-20152,'Phrase "'||upper(p_phrase)||'" has homographs.');  
  when phrase_error then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_res.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_ntp;  
  -- --------------------------------------------------------------  
  procedure get_rt (p_rt out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_thes_name in varchar2 default 'default') is  
  v_res ctx_thes.exp_tab; -- ctx_thes.rt result buffer  
  begin -- Check phrase for homographs if qualifier is absent  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  if instr(v_phrase,'(') = 0 and instr(v_phrase,')') = 0 then  
   if ctx_api.has_homographs (v_phrase, p_thes_name) then  
   raise term_has_homographs;  
   end if;  
  end if;  
  ctx_thes.rt(v_res, v_phrase, p_thes_name); -- Get RT's  
  if v_res.count = 1 then p_rt(1) := v_res(1).xphrase; -- If RT's absent, return phrase.  
  else -- If phrase has RT's, then return them all, exclude top - original phrase  
   for i in 2..v_res.last -- Extract only RT's  
   loop  
   p_rt(i-1) := v_res(i).xphrase; -- Let's count return array from 1  
   end loop;  
  end if;  
  v_res.delete; -- Free memory  
  exception   
  when term_has_homographs then  
   raise_application_error(-20152,'Phrase "'||upper(p_phrase)||'" has homographs.');  
  when phrase_error then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_res.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_rt;  
  -- --------------------------------------------------------------  
  procedure get_syn (p_syn out ctx_api.term_tab,  
           p_phrase in varchar2,  
           p_thes_name in varchar2 default 'default') is  
  v_res ctx_thes.exp_tab; -- ctx_thes.syn result buffer  
  begin -- Check phrase for homographs if qualifier is absent  
  v_phrase := trim(p_phrase); -- Trim leading and trailing spaces  
  if instr(v_phrase,'(') = 0 and instr(v_phrase,')') = 0 then  
   if ctx_api.has_homographs (v_phrase, p_thes_name) then  
   raise term_has_homographs;  
   end if;  
  end if;  
  ctx_thes.syn(v_res, v_phrase, p_thes_name); -- Get SYN's  
  if v_res.count = 1 then p_syn(1) := v_res(1).xphrase; -- If SYN's absent, return phrase.  
  else -- If phrase has SYN's, then return them all, exclude top - original phrase  
   for i in 2..v_res.last -- Extract only SYN's  
   loop  
   p_syn(i-1) := v_res(i).xphrase; -- Let's count return array from 1  
   end loop;  
  end if;  
  v_res.delete; -- Free memory  
  exception   
  when term_has_homographs then  
   raise_application_error(-20152,'Phrase "'||upper(p_phrase)||'" has homographs.');  
  when phrase_error then  
   raise_application_error(-20151,'Phrase "'||  
               upper(p_phrase)||'" not exist or specified thesaurus "'||  
               upper(p_thes_name)||'" not loaded.');  
  when others then  
   v_res.delete; -- Free memory  
   raise_application_error(-20155,'Error ORA' || SQLCODE || ' raised.');  
  end get_syn;  
  -- --------------------------------------------------------------  
  function search_string_parser (p_search_str in varchar2,  
                 p_query_mode in varchar2 default 'keyword',  
                 p_logical_op in varchar2 default 'and',  
                 p_query_opt in varchar2   
                 default ctx_api.c_query_op_about,  
                 p_expansion_level in number default 1,  
                 p_thes_name in varchar2 default 'default',  
                 p_refine_on in number   
                 default ctx_api.c_refine_off,  
                 p_exp_detail_on in number  
                 default ctx_api.c_exp_detail_off)   
                 return varchar2 deterministic is  
  -- --------------------------------------------  
  -- p_query_opt must be:  
  -- about (c_query_op_about)  
  -- bt (c_query_op_bt)  
  -- nt (c_query_op_nt)  
  -- rt (c_query_op_rt)  
  -- syn (c_query_op_syn)  
  -- p_expansion_level must be positive when   
  --          specified.  
  -- p_thes_name can be <= 30 characters.  
  -- Known issues:  
  -- 1. p_thes_name cannot contain "_" symbols,   
  --  it will be removed from result string!  
  -- 2. If p_exp_detail_on is enabled,  
  --  p_expansion_level value will be ignored.  
  -- 3. If p_query_opt in ('about','syn','rt') or  
  --  p_query_mode = 'keyword',  
  --  p_exp_detail_on will be ignored.  
  -- --------------------------------------------  
  type token_tab is table of varchar2(4000) index by binary_integer;  
  v_token token_tab; -- Token table  
   
  type exp_lvl_tab is table of number index by binary_integer;  
  v_exp_lvl exp_lvl_tab; -- Expansion levels table  
    
  type v_tt_rec is record (v_tt_token varchar2(4000),   
              v_tt_cnt number); -- TT record type  
  type v_tt_tab is table of v_tt_rec index by binary_integer; -- TT table type  
   
  v_tt v_tt_tab;      -- TT table  
  v_count pls_integer;   -- TT tokens counter  
  v_count2 pls_integer;  -- TT tokens counter 2  
  c_refine_level constant pls_integer := 1; -- Refine level constant. Higher value  
                       -- means more supercategories occurences.  
                       -- Minimum value is 1!  
  /* Note: TT is the Top Term abbreviation */  
  -- v_temp varchar2(4000); -- DEBUG  
  -- v_temp2 number;    -- DEBUG  
   
  v_buffer varchar2(4000); -- Search string buffer  
   
  v_temp_value varchar2(4000); -- Parser variables  
  v_temp_value2 varchar2(4000);  
  v_quotes pls_integer;    -- Quotation variable  
  n pls_integer;        -- Quotation parser counter  
  i pls_integer;        -- Common parser counter  
  j pls_integer;        -- Common final string forming counter  
   
  -- Note: Cannot call procedure hr_show_doc from javascript   
  -- when pass symbolic logical operators into parsed string  
  v_query_mode varchar2(10); -- Query mode buffer  
  c_or_operator constant varchar2(5) := ' or ';  -- OR operator  
  c_and_operator constant varchar2(5) := ' and '; -- AND operator  
  v_operator varchar2(5) := c_and_operator; -- Operator buffer, default AND  
  v_op_fin varchar2(5);           -- Final op buffer  
  v_exp_detail_on number(1) := p_exp_detail_on; -- Expansion detail flag buffer  
   
  v_ext1 varchar2(6); -- Open thes extension  
  v_ext2 varchar2(50); -- Close thes extension (including level and  
            -- thesaurus name)  
  v_expansion_level varchar2(10); -- Expansion functions level  
  v_thes_name varchar2(30);    -- Thesaurus name buffer  
   
  -- --------------------------------------------------------------  
  -- Delete unneeded delimiters in whole token  
  function del_delm(p_str in varchar2) return varchar2 is  
  v_str varchar2(4000);  
  begin  
  v_str := replace(p_str,',','');  
  while instr(v_str,' ') > 0 loop -- Remove unneeded spaces  
   v_str := replace(v_str,' ',' ');  
  end loop;  
  return v_str;  
  end del_delm;  
   
  ---- Escape key word in search string  
  function escape_key_word(p_str in varchar2) return varchar2 is  
  v_str varchar2(4000) := p_str; -- Init variable with parameter  
  begin  
  -- Escape all keywords  
  for i in v_reserved.first..v_reserved.last loop  
   v_str := replace(v_str,lower(v_reserved(i)),'{'||lower(v_reserved(i))||'}');  
  end loop;  
  return v_str;  
  end escape_key_word;  
   
  -- Remove key word in search string  
  function remove_key_word(p_str in varchar2) return varchar2 is  
  v_str varchar2(4000) := p_str; -- Init variable with parameter  
  begin  
  -- Remove all keywords  
  for i in v_reserved.first..v_reserved.last loop  
   v_str := replace(v_str,lower(v_reserved(i)),'');  
  end loop;  
  return v_str;  
  end remove_key_word;  
   
  -- Free tokens memory  
  procedure free_memory is  
  /* Uses for clean out PL/SQL tables in parser function */  
  begin  
  v_token.delete;  
  v_tt.delete;  
  if v_exp_detail_on = ctx_api.c_exp_detail_on then  
   v_exp_lvl.delete;  
  end if;  
  exception  
  when others then null;  
  end free_memory;  
   
  ----  
  begin /* MAIN BLOCK */  
  -- -----------------------------------------  
  -- Threat null search string  
  if nvl(length(p_search_str),0) = 0 then return null; end if;  
  -- Lowercase and trim spaces  
  v_buffer := trim(lower(p_search_str));  
  -- -----------------------------------------  
  -- Check query mode. If cannot identify, set KEYWORD by default.  
  if lower(p_query_mode) = 'keyword' or   
    lower(p_query_mode) = 'concept' then  
   v_query_mode := lower(p_query_mode);  
  else v_query_mode := 'keyword';  
  end if;  
  -- -----------------------------------------  
  -- Threat query mode  
  if v_query_mode = 'concept' then -- Check search string for keywords  
   v_buffer := escape_key_word(v_buffer); -- In CONCEPT mode ESCAPE keywords  
  else   
   v_buffer := remove_key_word(v_buffer); -- In KEYWORD mode REMOVE keywords  
   v_exp_detail_on := ctx_api.c_exp_detail_off; -- Turn off exp_detail in KEYWORD mode  
  end if;  
  -- -----------------------------------------  
  -- Threat logical operator  
  if lower(p_logical_op) = 'and' or instr(p_logical_op,'&') > 0 then  
   v_operator := c_and_operator;  
  elsif lower(p_logical_op) = 'or' or instr(p_logical_op,'|') > 0 then  
   v_operator := c_or_operator;  
  else               
   v_operator := c_or_operator;  
  end if;  
  -- -----------------------------------------  
  -- Threat ABOUT, RT, SYN and Expansion level flag  
  if (substr(lower(p_query_opt),1,2) = 'ab' or   
    substr(lower(p_query_opt),1,2) = 'rt' or   
    substr(lower(p_query_opt),1,2) = 'sy') then  
   v_exp_detail_on := ctx_api.c_exp_detail_off; -- Turn off exp_detail in ABOUT,RT,SYN mode  
  end if;  
  -- -----------------------------------------  
  /* First and last symbol check and clean */  
  if substr(v_buffer,1,1) = '|' then v_buffer := trim(both '|' from v_buffer);  
   elsif substr(v_buffer,1,1) = '&' then v_buffer := trim(both '&' from v_buffer);  
   elsif substr(v_buffer,1,1) = '~' then v_buffer := trim(both '~' from v_buffer);  
   elsif substr(v_buffer,1,1) = ',' then v_buffer := trim(both ',' from v_buffer);  
   elsif substr(v_buffer,1,1) = '=' then v_buffer := trim(both '=' from v_buffer);  
  end if;  
  -- -----------------------------------------  
  -- Clean search string from punctuation, garbage and DR engine keywords  
  v_buffer := trim(replace(v_buffer,'|', ' ')); -- Replace any search meta to space  
  v_buffer := trim(replace(v_buffer,'~', ' '));  
  v_buffer := trim(replace(v_buffer, '`', ' '));  
  v_buffer := trim(replace(v_buffer, '$', ' ')); -- STEM op  
  v_buffer := trim(replace(v_buffer, '>', ' ')); -- threshold op  
  v_buffer := trim(replace(v_buffer, '*', ' ')); -- weight op  
   
  v_buffer := trim(replace(v_buffer,':',' '));  
  v_buffer := trim(replace(v_buffer,';',' '));  
  v_buffer := trim(replace(v_buffer,':'',',' '));  
  v_buffer := trim(replace(v_buffer,'!',' ')); -- SOUNDEX op  
  v_buffer := trim(replace(v_buffer,'()','')); -- Suppress empty parenthes  
  v_buffer := trim(replace(v_buffer,'[]','')); -- Suppress empty parenthes  
  v_buffer := trim(replace(v_buffer,'{}','')); -- Suppress empty parenthes  
  v_buffer := trim(replace(v_buffer,'?',' ')); -- FUZZY equiv  
  v_buffer := trim(replace(v_buffer,'&',' ')); -- Must be before ',' threat  
  v_buffer := trim(replace(v_buffer,'+',' ')); -- ACCUM equiv  
  v_buffer := trim(replace(v_buffer,'\',' '));  
  --v_buffer := trim(replace(v_buffer,'-',' ')); -- This is printjoin char   
  v_buffer := trim(replace(v_buffer,',',' ')); -- Threat ','  
  v_buffer := trim(replace(v_buffer,'.',' '));  
  v_buffer := trim(replace(v_buffer,' and ',' ')); -- Remove logical ops  
  v_buffer := trim(replace(v_buffer,' or ',' ')); -- Remove logical ops  
  v_buffer := trim(replace(v_buffer,'""','" "')); -- Correct double quotation  
  -- -----------------------------------------  
   
  -- Parse '"' pairs  
  v_quotes := length(v_buffer)-length(replace(v_buffer,'"', ''));  
  -- if there are quotes and the quotes are balanced, we'll extract that  
  -- terms "as is" and save them into a phrases array  
  if (v_quotes > 0 or mod(v_quotes,2) = 0) then  
   -- If this predicate be AND, odd '"' will be  
   -- ignored all '"' pairs in next parsing stage  
   v_temp_value2 := v_buffer;  
   for i in 1..v_quotes/2 loop  
    n := instr(v_temp_value2,'"');  
    v_temp_value := v_temp_value || substr(v_temp_value2,1,n-1);  
    v_temp_value2 := substr(v_temp_value2,n+1);  
    n := instr(v_temp_value2,'"');  
    v_token(i) := trim(substr(v_temp_value2,1,n-1));  
    v_temp_value2 := substr(v_temp_value2,n+1);  
   end loop;  
    v_temp_value := v_temp_value || v_temp_value2;  
  else  
   v_temp_value := v_buffer;  
  end if;  
   
  v_buffer := v_temp_value; -- Let's parse next...  
     
  -- dbms_output.put_line('Rest string: '||v_temp_value); -- Debug output  
   
  -- Threat single phrase exception  
  if trim(v_buffer) is null and v_token.count > 0 then  
   goto Final;  
  end if;  
   
  -- Parse and get tokens in the rest string ...  
  if instr(v_buffer,' ') > 0 then -- Look ' ' delimiters  
   if v_token.count = 0 then i := 1;  
   else i := v_token.last + 1; -- Continue parsing  
   end if;  
   while instr(v_buffer,' ') > 0 loop  
   v_token(i) := substr(v_buffer,1,instr(v_buffer,' ')-1);  
   i := i + 1; -- Increment counter  
   v_buffer := substr(v_buffer,instr(v_buffer,' ')+1);  
   v_buffer := trim(v_buffer);  
   end loop;  
   -- Get the last token  
   if substr(v_buffer,length(v_buffer))='\' or  
    substr(v_buffer,length(v_buffer))='?' or  
    substr(v_buffer,length(v_buffer))=';' then  
   v_buffer :=  
    substr(v_buffer,1,length(v_buffer)-1)||'\'||  
    substr(v_buffer,length(v_buffer));  
   end if;  
   v_token(i):=v_buffer;  
  else  
   -- Only one word was in search string  
   if substr(v_buffer,length(v_buffer))='\' or  
    substr(v_buffer,length(v_buffer))='?' or  
    substr(v_buffer,length(v_buffer))=';' then  
   v_buffer := substr(v_buffer,1,length(v_buffer)-1)||'\'||  
         substr(v_buffer,length(v_buffer));  
   end if;  
   v_token(1) := v_buffer;  
  end if;  
  -- Parsing complete!  
   
  /* ====================== */  
  /* Query Expansion Detail */  
  /* ====================== */  
  -- Check and assign expansion level depends query option  
  -- (not used when ABOUT, SYN or RT specified)  
  -- Note: Only hierarchical relations has expansion levels!  
  if substr(lower(p_query_opt),1,2) <> 'ab' then  
   -- Check and assign thesaurus name  
   if substr(p_thes_name,1,30) = 'default' then  
   v_thes_name := null;  
   else  
   v_thes_name := ','||p_thes_name;  
   end if;  
   if (substr(lower(p_query_opt),1,2) = 'nt' or  -- If p_exp_detail_on is ON  
     substr(lower(p_query_opt),1,2) = 'bt') and -- and uses NT/BT query option  
     v_exp_detail_on = ctx_api.c_exp_detail_on -- and query expansion is ON...  
   then                      -- ... then expansion level up.  
   for v_count in v_token.first..v_token.last loop -- Init expansion levels array  
    v_exp_lvl(v_count) := ctx_api.search_expansion_level(v_token(v_count), p_thes_name);  
   end loop;  
   elsif (p_expansion_level = 0 or -- Also check zero (TT level) expansion level  
      p_expansion_level = 1 or  
      p_expansion_level < 0) or  
      (substr(lower(p_query_opt),1,2) <> 'nt' and  
      substr(lower(p_query_opt),1,2) <> 'bt') and  
      v_exp_detail_on = ctx_api.c_exp_detail_off  
   then v_expansion_level := null; -- Default expansion level  
   else  
   v_expansion_level := ','||to_char(p_expansion_level); -- set the same expansion level  
   end if;  
  elsif substr(lower(p_query_opt),1,2) = 'ab' then  
   -- ABOUT option not uses thesaurus and level  
   v_expansion_level := null;  
   v_thes_name := null;  
  end if;  
  /* ====================== */  
  /* Query Expansion Detail */  
  /* ====================== */  
   
  /* ===================== */  
  /* Query Context Refiner */  
  /* ===================== */  
  -- Refine query context using specified thesaurus  
  -- If query mode is CONCEPT and refine is enabled...  
  if v_query_mode = 'concept' and p_refine_on = 1 then  
   -- Get top terms for tokens in CONCEPT mode ...  
   for v_count in v_token.first..v_token.last loop  
   v_tt(v_count).v_tt_token := ctx_thes.tt(v_token(v_count), p_thes_name);  
   end loop;  
   -- Count TT's  
   for v_count in v_tt.first..v_tt.last loop  
   v_tt(v_count).v_tt_cnt := 0;  
   for v_count2 in v_tt.first..v_tt.last loop  
    if v_tt(v_count).v_tt_token = v_tt(v_count2).v_tt_token then  
    v_tt(v_count).v_tt_cnt := v_tt(v_count).v_tt_cnt + 1;  
    end if;  
   end loop;  
   end loop;  
   -- Check if all TT's unique and refine result if not  
   for v_count in v_tt.first..v_tt.last loop  
   -- For category counter=c_refine_level...  
   if v_tt(v_count).v_tt_cnt = c_refine_level then  
    v_token(v_count) := ''; -- ...delete source token!  
   end if;  
   end loop;  
  end if;  
  /* ===================== */  
  /* Query Context Refiner */  
  /* ===================== */  
   
  -- DEBUG output  
  -- dbms_output.put_line('TT''s:');  
  -- for v_count in v_token.first..v_token.last loop  
  -- v_temp := v_tt(v_count).v_tt_token;  
  -- v_temp2 := v_tt(v_count).v_tt_cnt;  
  -- dbms_output.put_line(v_temp||' '||v_temp2);  
  -- end loop;  
  -- dbms_output.put_line('TT Tokens in array:'||v_tt.count); -- Debug output  
  -- dbms_output.put_line('Tokens in array:'||v_token.count); -- Debug output  
  -- dbms_output.put_line('Exp levels in array:'||v_exp_lvl.count); -- Debug output  
   
  <<Final>>  
  loop  
  if v_query_mode = 'keyword' then  
   v_ext1 := '';  
   v_ext2 := '';  
  else  
   v_ext1 :=   
   case substr(lower(p_query_opt),1,2)  
   when 'ab' then 'about('  
   when 'bt' then 'bt('  
   when 'nt' then 'nt('  
   when 'rt' then 'rt('  
   when 'sy' then 'syn('  
   end; -- End case  
   if v_exp_detail_on = ctx_api.c_exp_detail_off then  
   v_ext2 := v_expansion_level||v_thes_name||')';  
   end if;  
  end if;  
  -- Threat final string  
  v_buffer := null;  
  for j in v_token.first..v_token.last loop  
   if substr(v_token(j),1,1) = '(' then  
   -- If token is single word in () (i.e.(cat)),  
   -- we'll ignore this token  
   v_token(j) := null;  
   end if;  
   if instr(v_token(j),'(') > 0 then -- Threat parenthes in qualifiers  
   v_token(j) := replace(v_token(j),'(',' (');  
   end if;  
   v_op_fin := v_operator;  
   if v_buffer is null then v_op_fin := null; end if; -- Start line?  
   -- Skip empty token entries. This also remove tokens cutted with context refiner.  
   if v_token(j) is not null then  
   if v_exp_detail_on = ctx_api.c_exp_detail_off then  -- Expansion OFF  
    v_buffer := v_buffer||v_op_fin||v_ext1||'{'||del_delm(v_token(j))||'}'||v_ext2;  
   elsif v_exp_detail_on = ctx_api.c_exp_detail_on then -- Expansion ON  
    v_buffer := v_buffer||v_op_fin||v_ext1||'{'||del_delm(v_token(j))||'}'||','||v_exp_lvl(j)||v_thes_name||')';  
   end if;  
   end if;   
  end loop;  
  exit when 1=1; -- Only one labeled loop  
  end loop;  
   
  -- Finally release token tables memory  
  free_memory;  
   
  -- -----------------------------------------  
  -- Wildcards must be removed in CONCEPT mode  
  if (instr(v_buffer,'%') > 0 or instr(v_buffer,'_') > 0)  
    and v_query_mode = 'concept' then  
  v_buffer := replace(v_buffer,'%','');  
  v_buffer := replace(v_buffer,'_','');  
  -- Workaround bug 10029.1  
  elsif (instr(v_buffer,'%') > 0 or instr(v_buffer,'_') > 0)  
     and v_query_mode = 'keyword' then  
  v_buffer := replace(v_buffer,'{','');  
  v_buffer := replace(v_buffer,'}','');  
  end if;  
  -- -----------------------------------------  
   
  return v_buffer;  
   
  exception  
  when text_error then  
   free_memory; -- Release memory and exit if text_error exception occurs  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  when others then   
   free_memory; -- When any others error, let's release memory  
   return null; -- and exit from function with null return  
  end search_string_parser; /* END MAIN BLOCK */  
  -- --------------------------------------------------------------  
  function term_counter (p_thes_name in varchar2 default 'default')  
             return number deterministic is  
  -- Specified thesaurus term counter  
  v_count number;  
  begin  
  select count(*)  
  into v_count  
  from ctx_thes_phrases  
  where thp_thesaurus = upper(p_thes_name)  
  group by thp_thesaurus;  
  return v_count;  
  exception  
  when no_data_found then  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  end term_counter;  
  -- --------------------------------------------------------------  
  procedure thes_loaded (p_ths_list out ctx_api.thes_tab) is  
  -- Get loaded thesauruses  
  begin  
  select ths_name  
  bulk collect into p_ths_list  
  from ctx_thesauri;  
  exception  
  when no_data_found then  
   raise_application_error(-20154,'No thesaurus found.');  
  end thes_loaded;  
  -- --------------------------------------------------------------  
 end ctx_api;  
 /  
   
 show errors  
   

Замечание Это было замечено, но не было реализовано: Токен игнорируется, если он начинается с открытого апострофа '. Можно попытаться его экранировать при очистке исходной строки.

Принцип работы собственно парсера:
  1. Поисковый запрос очищается от мусора и ключевых слов
  2. Обрабатываются особые и граничные случаи элементов запроса
  3. В зависимости от выбранного режима работы парсера выполняется:
    1. Либо разбивка непосредственно на токены с обработкой заключенных в кавычки слов и словосочетаний - они выделяются в отдельные токены и обрабатываются как точное соответствие
    2. Либо дополнительно происходит выделение токенов с квалификаторами (подобно тому, как это реализовано в Википедии) при использовании тезаурусов, могущих содержать омонимы/гомографы
  4. В случае, если выбран режим уточнения запроса и имеется загруженный тезаурус, производится тематический анализ принадлежности токенов одной теме на основе тезауруса и отбрасывание токенов, тематически не относящихся к количественно доминирующей в токенах запроса тематике.
  5. Затем из распарсенных токенов, после вышеприведенной обработки, снова собирается строка запроса и возвращается функцией для передачи непосредственно в Oracle Text и последующего исполнения.
Немного дополнительной информации по применению:

 ВНИМАНИЕ: Пакет использует процедуры пакета CTX_THES.  Для  
 корректной загрузки и работы пакета необходимо дать прямой  
 грант на пакет CTX_THES:  
   
 grant execute on ctx_thes to <user>;  
   
 Гранта роли CTXAPP недостаточно! (роль грантуется целевой  
 схеме установочным скриптом ввиду необходимости доступа  
 функций пакета к представлениям схемы CTXSYS)  
   
 Замечание: Данный грант автоматически дается при установке  
 парсера скриптом inst_parser.*  
   
 Замечание 1: Если парсер выполняются в режиме, использующим  
 тезаурус  (CONTEXT  query  mode/уточнение  контекста  
 по тезаурусу), при отсутствии в базе данных загруженного  
 тезауруса,  возбуждается  ошибка  ORA-20150.  
 Остальные подпрограммы пакета также используют тезаурус  
 и при отсутствии загруженного тезауруса возбуждают ошибку  
 20150. Следует иметь в виду, что тезаурус не обязательно  
 имеет имя DEFAULT, в этом случае при вызове подпрограмм  
 (включая парсер) необходимо определять имя используемого  
 тезауруса.  
   
 Замечание 2:   Константа ctx_nt_terms пакета определяет  
 количество терминов NT уровня иерархии тезауруса, на котором  
 следует  остановиться  при  выполнении  автоматического  
 расширения иерархических функций BT/NT функциями пакета  
 search_expansion_level, search_expansion_term,  
 search_string_parser.   
 По  умолчанию  функции останавливаются на том уровне,  
 BT которого  содержит свыше 5 терминов NT. Как правило,  
 этого достаточно для типовых применений. Если значения  
 по умолчанию не достаточно, необходимо изменить константу  
 (отредактировав спецификацию пакета) и перезагрузить пакет  
 (спецификацию и тело).  
   
 Замечание 3:  Разумеется, до установки пакета база данных  
 должна содержать установленную опцию Oracle Text (ConText).  
 Пакет содержит определения типов данных на основе типов  
 Oracle Text. При выполнении установки пакета выполняется  
 проверка существования Oracle Text в целевой БД.  
   
 Особенности поведения:  
 ======================  
   
 1. Параметр p_thes_name не может содержать символы "_", они  
 будут  удалены  из  результирующей строки, возвращаемой  
 парсером.  
   
 2. Если параметр p_exp_detail_on включен (равен 1), параметр  
 p_expansion_level будет игнорироваться.  
   
 3. Если режим расширения запроса p_query_opt='about','syn'  
 или  'rt',  либо если p_query_mode = 'keyword' (поиск  
 по  ключевым  словам),  параметр  p_exp_detail_on  will  
 игнорируется.  
   
   

Упомянутый выше скрипт установки inst_api.sql:

 rem --------------------------------------------------------------------------  
 rem -- PROJECT_NAME: CTX_API                        --  
 rem -- RELEASE_VERSION: 1.0.0.5                       --  
 rem -- RELEASE_STATUS: Release                       --  
 rem --                                   --  
 rem -- REQUIRED_ORACLE_VERSION: 10.1.0.x                  --  
 rem -- MINIMUM_ORACLE_VERSION: 9.2.0.3                   --  
 rem -- MAXIMUM_ORACLE_VERSION: 11.x.x.x                   --  
 rem -- PLATFORM_IDENTIFICATION: Generic                   --  
 rem --                                   --  
 rem -- IDENTIFICATION: inst_api.sql                     --  
 rem -- DESCRIPTION: This script installs CTX API into specified existin   --  
 rem --       schema. Must be run as SYS user with SYSDBA priv.    --  
 rem --       This script are interactive. Command-line version is  --  
 rem --       inst_parser_c.sql                    --  
 rem --                                   --  
 rem -- INTERNAL_FILE_VERSION: 0.0.0.3                    --  
 rem --                                   --  
 rem -- COPYRIGHT: Yuri Voinov (C) 2004, 2016                --  
 rem --                                   --  
 rem -- MODIFICATIONS:                            --  
 rem -- 23.05.2016 -Update copyright.                    --  
 rem -- 19.12.2006 -Add Oracle Text installation check.           --  
 rem -- 22.10.2006 -Fixed minor bug with GRANT EXECUTE ON ctx_thes package. --  
 rem --       Check grant if not already granted added.        --  
 rem -- 20.08.2006 -Initial code written.                  --  
 rem --------------------------------------------------------------------------  
   
 set verify off  
   
 whenever sqlerror exit sql.sqlcode  
   
 spool inst_api.log  
   
 prompt ============================================  
 prompt CTX_API installation ...  
 prompt ============================================  
 prompt --------------------------------------------  
   
 prompt Specify target user schema to install API  
 prompt  
 prompt You must specify sys password and ORACLE SID  
 prompt for database to run this script  
   
 prompt --------------------------------------------  
   
 accept schema_name char prompt 'Input target user schema:'  
   
 accept ora_sid char prompt 'Input ORACLE SID:'  
   
 accept sys_password char prompt 'Input SYS password:' hide  
   
 connect sys/&&sys_password@&&ora_sid as sysdba  
   
 set serveroutput on  
   
 declare  
  -- Check Oracle Text installed  
  v_ctx varchar2(30);  
 begin  
  select object_name  
  into v_ctx  
  from all_objects   
  where object_name = 'CTX_THES'  
   and object_type = 'PACKAGE';  
  dbms_output.put_line('Oracle Text Installed. Check OK.');  
 exception  
  when no_data_found then  
  raise_application_error(-20111,'Oracle Text does not installed.');  
 end;  
 /  
   
 set serveroutput off  
   
 declare  
  --  
  -- Check user and select target schema stuff.  
  --  
  c_ctx_role constant varchar2(30) := 'ctxapp'; -- CTXAPP role name  
  v_user varchar2(30); -- Target user buffer  
  v_ddl varchar2(255); -- DDL buffer  
  v_priv varchar2(40); -- Priv buffer  
 begin  
  begin  
  select username -- Check target user exists  
  into v_user  
  from all_users  
  where username = upper('&&schema_name');  
  exception  
  when too_many_rows then -- Check most suggest username  
   select max(username)  
   into v_user  
   from all_users  
   where username = upper('&&schema_name');  
  end;  
  --  
  -- Make grants  
  --  
  begin  
  -- Verify if grant is not already for target user  
  select privilege  
  into v_priv  
  from all_tab_privs  
  where grantee = upper('&&schema_name')  
   and privilege = 'EXECUTE'   
   and table_name = 'CTX_THES'  
   and grantor = 'CTXSYS';  
  exception  
  -- If not granted, then do it now  
  when no_data_found then  
   v_ddl := 'grant execute on ctxsys.ctx_thes to &&schema_name';  
   execute immediate(v_ddl);  
  end;  
  --  
  -- Grant CTXAPP role to target user  
  v_ddl := 'grant '||c_ctx_role||' to &&schema_name';  
  execute immediate(v_ddl);  
  -- Grant select on thes view to target user  
  v_ddl := 'grant select on ctxsys.ctx_thesauri to &&schema_name';  
  execute immediate(v_ddl);  
  -- Grant select on phrases view to target user  
  v_ddl := 'grant select on ctxsys.ctx_thes_phrases to &&schema_name';  
  execute immediate(v_ddl);  
  --  
  -- Set schema for installation  
  --  
  v_ddl := 'alter session set current_schema = '||v_user||'';  
  execute immediate(v_ddl);  
 exception  
  -- Exceptions handlers  
  when no_data_found then raise_application_error(-20110,'Target user specified NOT FOUND!');  
  when others then raise_application_error(-20120,'Error ORA'||SQLCODE);  
 end;  
 /  
   
 prompt  
 prompt CTX_API package specification loading...  
   
 @@ctx_api.sql  
   
 prompt  
 prompt CTX_API package body loading...  
   
@@prvtctxapi.pls  
   
rem @@prvtctxapi.plb  
   
 prompt ============================================  
 prompt CTX_API installation done.   
 prompt ============================================  
   
 spool off  
   
 disconnect  
   
 exit    

Пользуйтесь на здоровье. Лицензия на все это - BSD:

 -- COPYRIGHT: Yuri Voinov (C) 2004, 2016                   --  
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

PS. В третьей части статьи будут выложена документация по пакету, а в четвертой части будут рассмотрены вопросы построения и использования тезаурусов, реализация их поддержки в Oracle Text, реализация в виде WSDL-сервиса и другие смежные вопросы.

четверг, 12 мая 2016 г.

Solaris: /var/tmp housekeeping

Подавляющее большинство админов даже не заглядывало в /var/tmp, верно я говорю, коллеги? 

Между тем, следует знать, что конкретно на Солярис (да и на большинстве других *NIX ОС), в отличие от /tmp (только на Солярис она находится в RAM и очищается при каждом рестарте), эта директория не очищается автоматически. Совсем.

Если находится в аптайме длительное время, и при этом работает под приличной нагрузкой, а вы пожадничали на слайс /var, вы весьма скоро можете столкнуться с тем, что на /var место закончилось.

Что грозит, в ряде случаев, остановкой сервисов, падением сервера и другими приятными вещами.

Как я говорю в подобных случаях - пусть сервер пашет, он железянный.

Загоните задачу housekeepeng в cron, примерно вот такую:
 # Cleanup /var/tmp. Running weekly at 21:00 Saturday  
 21 0 * * 6 /bin/find /var/tmp -type f -atime +7 -mtime +7 -exec /bin/rm -f {} \; >/dev/null 2>&1  
 21 0 * * 6 /bin/find /var/tmp -type d -atime +7 -mtime +7 -exec /bin/rm -r {} \; >/dev/null 2>&1  

Что делает эта пара задач?

Она смотрит в /var/tmp, и если находит файлы или директории с временем доступа либо временем модификации старше 7 суток, то удаляет их не задавая лишних вопросов.

Все просто. Если файлы или директории в /var/tmp более недели не понадобились - они не нужны.

воскресенье, 1 мая 2016 г.

Не слишком верьте UNIX Top

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

Да, я говорю о UNIX top.

Хочу заметить следующее.

  1. Она написана во времена, когда компания DEC еще была жива. Т.е. во времена Она.
  2. Кроме беты 1 версии 3.8, которой самой сто лет в обед и которая знаменита непрерывно текущей памятью и постоянными дампами, единственной стабильной версией является 3.7 - еще более старая и не имеющая ни малейшего понятия о современных ядрах.
  3. Эта утилита брешет как Лев Троцкий на третьем партийном съезде и вводит в заблуждение тех, кто ей слепо доверят.

Я поделюсь своими долговременными наблюдениями за показаниями top 3.7 и мы сравним ее показания с другими утилитами, а также сделаем соответствующие выводы.

Итак, смотрите сюда:



Что вы видите? На первый взгляд, система кажется не загруженной. Однако что-то не так.

Смотрите внимательней.

Да, free swap меньше, чем total swap.

То есть - вероятно, происходит свопинг, не так ли?

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

Однако давайте не будем принимать на веру показания top и выполним перекрестную проверку средствами других, штатных для системы, утилит:


Что мы видим в первую очередь? Мы не видим высокой дисковой активности, верно?

Наш своп находится на одном из системных дисков, следовательно, мы бы видели эту активность, если бы она имела место, хотя и косвенно, в общем потоке IO операций.

Хорошо, посмотрим теперь на общую картину утилитой vmstat:


Что мы видим здесь? Дисковая активность действительно имеется и показатели примерно соотносятся с тем, что показывает iostat, однако активность свопинга и пейджинга равна нулю (столбцы si, so - swap in и swap out, и pi,po - page in и page out соответственно).

То есть в действительности никакого свопинга и даже пейджинга в системе не наблюдается в принципе, хотя, если вернуться к показаниям top, видно, что свободной памяти в системе совсем немного.

Как, в таком случае, следует понимать показания top в начале статьи?

  1. Необходимо помнить о том, что механизмы свопинга различным образом реализованы в различных ядрах. Эти механизмы сильно завязаны на подсистему планировщика заданий (scheduler) и подсистему аллокатора памяти ядра OS.
  2. top зачастую не содержит специфических знаний об особенностях реализации данных механизмов и показывает страницы, которые распределены в области подкачки, но ничего не сообщает о том, как они используются и используются ли вообще.
  3. Таким образом, top не может и не показывает реальной картины, есть ли в данный момент свопинг в системе или нет, если не смотреть на другие объективные показатели - например, на время ядра (kernel time), количество процессов на процессорах, реальные задержки отклика процессов и т.п. Причем даже и в этом случае картина может быть необъективной без контрольных проверок другими инструментами.
Возвращаясь к первоначальному примеру.

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

Может показаться, что этот момент не за горами, так как свободной памяти в системе, согласно показаниям top, совсем немного.

Однако, следует иметь в виду (и это опять-таки специфика систем, причем их большинства), что часть памяти, которая показывается как занятая, причем значительная часть, в действительности занята кэшами файловых систем, и, в большинстве случаев, ядро освобождает такую память после синхронизации кэшей на диски по первому требованию процессов. Механизмы выделения страниц в действительности несколько более сложные, однако в общих чертах происходящие процессы именно таковы.

То есть до начала реального свопинга в действительности весьма и весьма далеко.


Более того, при отсутствии реальных утечек памяти мне приходилось наблюдать картину, как система, всего лишь с десятком мегабайт свободной оперативной памяти, месяцами находится в стабильном состоянии, не начиная свопинга вовсе.

Да, может начаться процесс пейджинга как результат, в частности, фрагментации оперативной памяти в процессе работы, увеличивающий kernel time и уменьшающий время отклика системы (впрочем, не очень значительно), однако это совсем другая история, впрямую со свопингом зачастую не связанная и не обязательно приводящая к свопингу в конечном итоге.

Oracle Text: Высокоскоростной парсер поисковых запросов, часть I

Хотя эта разработка достаточно древняя, она все еще актуальна и может быть неплохим наглядным пособием по программированию на PL/SQL. Oracle Text активно применяется, кроме того, решение все еще превосходит по скорости функции Oracle с регулярными выражениями.

Данная разработка была выполнена в 2008-2009 году для прикладных применений совместно с Web-based database applications for Oracle.

Oracle Text представляет собой продвинутый механизм полнотекстового поиска с использованием контекстных индексов по столбцам таблицы БД, в том числе содержащих BLOB. 

Однако его прямое использование осложнено тем фактом, что в программном интерфейсе отсутствует какой-либо парсер собственно поисковых запросов (что, в общем, неудивительно), и выставлять Oracle Text непосредственно пользователям весьма опрометчиво. Как минимум, работоспособность Oracle Text в части выполнения запросов вида "абсолютно что угодно от пользователя" под серьезным сомнением.

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

Для начала опишем необходимые нам для парсинга структуры данных, вместе с заголовком функции (парсер нужен именно в виде функции, понятно, почему):

  -- --------------------------------------------------------------  
  function search_string_parser (p_search_str in varchar2,  
                 p_query_mode in varchar2 default 'keyword',  
                 p_logical_op in varchar2 default 'and',  
                 p_query_opt in varchar2   
                 default ctx_api.c_query_op_about,  
                 p_expansion_level in number default 1,  
                 p_thes_name in varchar2 default 'default',  
                 p_refine_on in number   
                 default ctx_api.c_refine_off,  
                 p_exp_detail_on in number  
                 default ctx_api.c_exp_detail_off)   
                 return varchar2 deterministic is  
  -- --------------------------------------------  
  -- p_query_opt must be:  
  -- about (c_query_op_about)  
  -- bt (c_query_op_bt)  
  -- nt (c_query_op_nt)  
  -- rt (c_query_op_rt)  
  -- syn (c_query_op_syn)  
  -- p_expansion_level must be positive when   
  --          specified.  
  -- p_thes_name can be <= 30 characters.  
  -- Known issues:  
  -- 1. p_thes_name cannot contain "_" symbols,   
  --  it will be removed from result string!  
  -- 2. If p_exp_detail_on is enabled,  
  --  p_expansion_level value will be ignored.  
  -- 3. If p_query_opt in ('about','syn','rt') or  
  --  p_query_mode = 'keyword',  
  --  p_exp_detail_on will be ignored.  
  -- --------------------------------------------  
  type token_tab is table of varchar2(4000) index by binary_integer;  
  v_token token_tab; -- Token table  
   
  type exp_lvl_tab is table of number index by binary_integer;  
  v_exp_lvl exp_lvl_tab; -- Expansion levels table  
    
  type v_tt_rec is record (v_tt_token varchar2(4000),   
              v_tt_cnt number); -- TT record type  
  type v_tt_tab is table of v_tt_rec index by binary_integer; -- TT table type  
   
  v_tt v_tt_tab;      -- TT table  
  v_count pls_integer;   -- TT tokens counter  
  v_count2 pls_integer;  -- TT tokens counter 2  
  c_refine_level constant pls_integer := 1; -- Refine level constant. Higher value  
                       -- means more supercategories occurences.  
                       -- Minimum value is 1!  
  /* Note: TT is the Top Term abbreviation */  
  -- v_temp varchar2(4000); -- DEBUG  
  -- v_temp2 number;    -- DEBUG  
   
  v_buffer varchar2(4000); -- Search string buffer  
   
  v_temp_value varchar2(4000); -- Parser variables  
  v_temp_value2 varchar2(4000);  
  v_quotes pls_integer;    -- Quotation variable  
  n pls_integer;        -- Quotation parser counter  
  i pls_integer;        -- Common parser counter  
  j pls_integer;        -- Common final string forming counter  
   
  -- Note: Cannot call procedure hr_show_doc from javascript   
  -- when pass symbolic logical operators into parsed string  
  v_query_mode varchar2(10); -- Query mode buffer  
  c_or_operator constant varchar2(5) := ' or ';  -- OR operator  
  c_and_operator constant varchar2(5) := ' and '; -- AND operator  
  v_operator varchar2(5) := c_and_operator; -- Operator buffer, default AND  
  v_op_fin varchar2(5);           -- Final op buffer  
  v_exp_detail_on number(1) := p_exp_detail_on; -- Expansion detail flag buffer  
   
  v_ext1 varchar2(6); -- Open thes extension  
  v_ext2 varchar2(50); -- Close thes extension (including level and  
            -- thesaurus name)  
  v_expansion_level varchar2(10); -- Expansion functions level  
  v_thes_name varchar2(30);    -- Thesaurus name buffer  
   
  -- --------------------------------------------------------------  

Как вы понимаете, это фрагмент окончательного варианта парсера, который является частью пакета PL/SQL. Пакет будет рассмотрен в следующих частях и доступен для скачивания.

Ключевыми структурами данных являются PL/SQL таблицы, которые будут использоваться для токенов (здесь: отдельных слов) поискового запроса в процессе парсинга:

  type token_tab is table of varchar2(4000) index by binary_integer;  
  v_token token_tab; -- Token table  
   
  type v_tt_rec is record (v_tt_token varchar2(4000),    
        v_tt_cnt number); -- TT record type   
  type v_tt_tab is table of v_tt_rec index by binary_integer; -- TT table type   
     
  v_tt v_tt_tab;   -- TT table   
  v_count pls_integer;  -- TT tokens counter   
  v_count2 pls_integer; -- TT tokens counter 2   


Для начала напишем несколько подпрограмм, которые понадобятся нам в дальнейшем:

  -- Delete unneeded delimiters in whole token  
  function del_delm(p_str in varchar2) return varchar2 is  
  v_str varchar2(4000);  
  begin  
  v_str := replace(p_str,',','');  
  while instr(v_str,' ') > 0 loop -- Remove unneeded spaces  
   v_str := replace(v_str,' ',' ');  
  end loop;  
  return v_str;  
  end del_delm;  
   
  ---- Escape key word in search string  
  function escape_key_word(p_str in varchar2) return varchar2 is  
  v_str varchar2(4000) := p_str; -- Init variable with parameter  
  begin  
  -- Escape all keywords  
  for i in v_reserved.first..v_reserved.last loop  
   v_str := replace(v_str,lower(v_reserved(i)),'{'||lower(v_reserved(i))||'}');  
  end loop;  
  return v_str;  
  end escape_key_word;  
   
  -- Remove key word in search string  
  function remove_key_word(p_str in varchar2) return varchar2 is  
  v_str varchar2(4000) := p_str; -- Init variable with parameter  
  begin  
  -- Remove all keywords  
  for i in v_reserved.first..v_reserved.last loop  
   v_str := replace(v_str,lower(v_reserved(i)),'');  
  end loop;  
  return v_str;  
  end remove_key_word;  
   
  -- Free tokens memory  
  procedure free_memory is  
  /* Uses for clean out PL/SQL tables in parser function */  
  begin  
  v_token.delete;  
  v_tt.delete;  
  if v_exp_detail_on = ctx_api.c_exp_detail_on then  
   v_exp_lvl.delete;  
  end if;  
  exception  
  when others then null;  
  end free_memory;  
   

Важной частью собственно парсера здесь являются процедуры удаления зарезервированных слов Oracle Text из запроса, экранирования ключевых слов OracleText и явного освобождения памяти токенов по завершении работы парсера, без этого существует достаточно высокий риск утечки памяти.

Массив ключевых слов OracleText определяется как:

  type ReservedWordList is table of varchar2(8);  
  v_reserved ReservedWordList := ReservedWordList('ABOUT','ACCUM','BT','BTG','BTI','BTP','FUZZY','HASPATH',  
                  'INPATH','MDATA','MINUS','NEAR','NT','NTG','NTI','NTP','PT','RT','SQE',  
                  'SYN','TR','TRSYN','TT','WITHIN');  
   

Собственно, просто для безопасности я бы, пожалуй, добавил к списку зарезервированных слов, подлежащих безусловному удалению из поискового запроса, ключевое слово UNION. Просто в порядке паранойи от SQL Injection. Почему это не было сделано в окончательном варианте - чтобы кардинально не менять смысл поисковых запросов на английском языке. Собственно, достаточно уже того, что ключевые слова about, within, near, fuzzy и minus сами по себе достаточно сильно меняют смысл поискового запроса, будучи удаленными. Однако, с учетом того факта, что первоначальным предназначением парсера был разбор поисковых запросов на русском языке, с этим можно в достаточной степени мириться. Да, это недостаток существующей реализации.

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

  begin /* MAIN BLOCK */  
  -- -----------------------------------------  
  -- Threat null search string  
  if nvl(length(p_search_str),0) = 0 then return null; end if;  
  -- Lowercase and trim spaces  
  v_buffer := trim(lower(p_search_str));  
  -- -----------------------------------------  
  -- Check query mode. If cannot identify, set KEYWORD by default.  
  if lower(p_query_mode) = 'keyword' or   
    lower(p_query_mode) = 'concept' then  
   v_query_mode := lower(p_query_mode);  
  else v_query_mode := 'keyword';  
  end if;  
  -- -----------------------------------------  
  -- Threat query mode  
  if v_query_mode = 'concept' then -- Check search string for keywords  
   v_buffer := escape_key_word(v_buffer); -- In CONCEPT mode ESCAPE keywords  
  else   
   v_buffer := remove_key_word(v_buffer); -- In KEYWORD mode REMOVE keywords  
   v_exp_detail_on := ctx_api.c_exp_detail_off; -- Turn off exp_detail in KEYWORD mode  
  end if;  
  -- -----------------------------------------  
  -- Threat logical operator  
  if lower(p_logical_op) = 'and' or instr(p_logical_op,'&') > 0 then  
   v_operator := c_and_operator;  
  elsif lower(p_logical_op) = 'or' or instr(p_logical_op,'|') > 0 then  
   v_operator := c_or_operator;  
  else               
   v_operator := c_or_operator;  
  end if;  
  -- -----------------------------------------  
  -- Threat ABOUT, RT, SYN and Expansion level flag  
  if (substr(lower(p_query_opt),1,2) = 'ab' or   
    substr(lower(p_query_opt),1,2) = 'rt' or   
    substr(lower(p_query_opt),1,2) = 'sy') then  
   v_exp_detail_on := ctx_api.c_exp_detail_off; -- Turn off exp_detail in ABOUT,RT,SYN mode  
  end if;  
  -- -----------------------------------------  
  /* First and last symbol check and clean */  
  if substr(v_buffer,1,1) = '|' then v_buffer := trim(both '|' from v_buffer);  
   elsif substr(v_buffer,1,1) = '&' then v_buffer := trim(both '&' from v_buffer);  
   elsif substr(v_buffer,1,1) = '~' then v_buffer := trim(both '~' from v_buffer);  
   elsif substr(v_buffer,1,1) = ',' then v_buffer := trim(both ',' from v_buffer);  
   elsif substr(v_buffer,1,1) = '=' then v_buffer := trim(both '=' from v_buffer);  
  end if;  
  -- -----------------------------------------  
  -- Clean search string from punctuation, garbage and DR engine keywords  
  v_buffer := trim(replace(v_buffer,'|', ' ')); -- Replace any search meta to space  
  v_buffer := trim(replace(v_buffer,'~', ' '));  
  v_buffer := trim(replace(v_buffer, '`', ' '));  
  v_buffer := trim(replace(v_buffer, '$', ' ')); -- STEM op  
  v_buffer := trim(replace(v_buffer, '>', ' ')); -- threshold op  
  v_buffer := trim(replace(v_buffer, '*', ' ')); -- weight op  
   
  v_buffer := trim(replace(v_buffer,':',' '));  
  v_buffer := trim(replace(v_buffer,';',' '));  
  v_buffer := trim(replace(v_buffer,':'',',' '));  
  v_buffer := trim(replace(v_buffer,'!',' ')); -- SOUNDEX op  
  v_buffer := trim(replace(v_buffer,'()','')); -- Suppress empty parenthes  
  v_buffer := trim(replace(v_buffer,'[]','')); -- Suppress empty parenthes  
  v_buffer := trim(replace(v_buffer,'{}','')); -- Suppress empty parenthes  
  v_buffer := trim(replace(v_buffer,'?',' ')); -- FUZZY equiv  
  v_buffer := trim(replace(v_buffer,'&',' ')); -- Must be before ',' threat  
  v_buffer := trim(replace(v_buffer,'+',' ')); -- ACCUM equiv  
  v_buffer := trim(replace(v_buffer,'\',' '));  
  --v_buffer := trim(replace(v_buffer,'-',' ')); -- This is printjoin char   
  v_buffer := trim(replace(v_buffer,',',' ')); -- Threat ','  
  v_buffer := trim(replace(v_buffer,'.',' '));  
  v_buffer := trim(replace(v_buffer,' and ',' ')); -- Remove logical ops  
  v_buffer := trim(replace(v_buffer,' or ',' ')); -- Remove logical ops  
  v_buffer := trim(replace(v_buffer,'""','" "')); -- Correct double quotation  
  -- -----------------------------------------  
   

Данные операции совершенно необходимы, например, очистка поисковой строки от мусора, избыточных пробелов, приведение строки к нижнему регистру, удаление символов, могущих быть интерпретированными как спецсимволы OracleText либо приводящих к ошибке выполнения запроса.

Хотя кажется, что это много кода, реально для выполнения данных операций требуется всего около 0.01 секунды в максимуме, на достаточно слабых процессорах.

Далее мы начинаем парсинг.

Мне хотелось реализовать функциональность поиска Google, когда часть запроса, взятая в кавычки, используется для поиска на безусловное соответствие. С этой целью сперва мы проверяем строку на наличие парных кавычек и выбираем части запроса в кавычках во внутренний буфер:

  -- Parse '"' pairs  
  v_quotes := length(v_buffer)-length(replace(v_buffer,'"', ''));  
  -- if there are quotes and the quotes are balanced, we'll extract that  
  -- terms "as is" and save them into a phrases array  
  if (v_quotes > 0 or mod(v_quotes,2) = 0) then  
   -- If this predicate be AND, odd '"' will be  
   -- ignored all '"' pairs in next parsing stage  
   v_temp_value2 := v_buffer;  
   for i in 1..v_quotes/2 loop  
    n := instr(v_temp_value2,'"');  
    v_temp_value := v_temp_value || substr(v_temp_value2,1,n-1);  
    v_temp_value2 := substr(v_temp_value2,n+1);  
    n := instr(v_temp_value2,'"');  
    v_token(i) := trim(substr(v_temp_value2,1,n-1));  
    v_temp_value2 := substr(v_temp_value2,n+1);  
   end loop;  
    v_temp_value := v_temp_value || v_temp_value2;  
  else  
   v_temp_value := v_buffer;  
  end if;  
   
  v_buffer := v_temp_value; -- Let's parse next...  
     
  -- dbms_output.put_line('Rest string: '||v_temp_value); -- Debug output  
   
  -- Threat single phrase exception  
  if trim(v_buffer) is null and v_token.count > 0 then  
   goto Final;  
  end if;  
   
   

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

Основная часть парсера. Обрабатываем остаток строки, после того, как выбрали из нее все закавыченные токены для непосредственной передачи в OracleText:

  -- Parse and get tokens in the rest string ...  
  if instr(v_buffer,' ') > 0 then -- Look ' ' delimiters  
   if v_token.count = 0 then i := 1;  
   else i := v_token.last + 1; -- Continue parsing  
   end if;  
   while instr(v_buffer,' ') > 0 loop  
   v_token(i) := substr(v_buffer,1,instr(v_buffer,' ')-1);  
   i := i + 1; -- Increment counter  
   v_buffer := substr(v_buffer,instr(v_buffer,' ')+1);  
   v_buffer := trim(v_buffer);  
   end loop;  
   -- Get the last token  
   if substr(v_buffer,length(v_buffer))='\' or  
    substr(v_buffer,length(v_buffer))='?' or  
    substr(v_buffer,length(v_buffer))=';' then  
   v_buffer :=  
    substr(v_buffer,1,length(v_buffer)-1)||'\'||  
    substr(v_buffer,length(v_buffer));  
   end if;  
   v_token(i):=v_buffer;  
  else  
   -- Only one word was in search string  
   if substr(v_buffer,length(v_buffer))='\' or  
    substr(v_buffer,length(v_buffer))='?' or  
    substr(v_buffer,length(v_buffer))=';' then  
   v_buffer := substr(v_buffer,1,length(v_buffer)-1)||'\'||  
         substr(v_buffer,length(v_buffer));  
   end if;  
   v_token(1) := v_buffer;  
  end if;  
  -- Parsing complete!  
   

Завершающая стадия парсинга:

  <<Final>>  
  loop  
  if v_query_mode = 'keyword' then  
   v_ext1 := '';  
   v_ext2 := '';  
  else  
   v_ext1 :=   
   case substr(lower(p_query_opt),1,2)  
   when 'ab' then 'about('  
   when 'bt' then 'bt('  
   when 'nt' then 'nt('  
   when 'rt' then 'rt('  
   when 'sy' then 'syn('  
   end; -- End case  
   if v_exp_detail_on = ctx_api.c_exp_detail_off then  
   v_ext2 := v_expansion_level||v_thes_name||')';  
   end if;  
  end if;  
  -- Threat final string  
  v_buffer := null;  
  for j in v_token.first..v_token.last loop  
   if substr(v_token(j),1,1) = '(' then  
   -- If token is single word in () (i.e.(cat)),  
   -- we'll ignore this token  
   v_token(j) := null;  
   end if;  
   if instr(v_token(j),'(') > 0 then -- Threat parenthes in qualifiers  
   v_token(j) := replace(v_token(j),'(',' (');  
   end if;  
   v_op_fin := v_operator;  
   if v_buffer is null then v_op_fin := null; end if; -- Start line?  
   -- Skip empty token entries. This also remove tokens cutted with context refiner.  
   if v_token(j) is not null then  
   if v_exp_detail_on = ctx_api.c_exp_detail_off then  -- Expansion OFF  
    v_buffer := v_buffer||v_op_fin||v_ext1||'{'||del_delm(v_token(j))||'}'||v_ext2;  
   elsif v_exp_detail_on = ctx_api.c_exp_detail_on then -- Expansion ON  
    v_buffer := v_buffer||v_op_fin||v_ext1||'{'||del_delm(v_token(j))||'}'||','||v_exp_lvl(j)||v_thes_name||')';  
   end if;  
   end if;   
  end loop;  
  exit when 1=1; -- Only one labeled loop  
  end loop;  
   
  -- Finally release token tables memory  
  free_memory;  
      

Убираем пустые токены, если таковые вдруг образовались в процессе разбора, формируем финальную строку и освобождаем промежуточные таблицы.

Затем возвращаем результат и обрабатываем исключения, если таковые возникли:

  return v_buffer;  
   
  exception  
  when text_error then  
   free_memory; -- Release memory and exit if text_error exception occurs  
   raise_application_error(-20150,'Oracle Text error. Possible specified thesaurus not loaded.');  
  when others then   
   free_memory; -- When any others error, let's release memory  
   return null; -- and exit from function with null return  
  end search_string_parser; /* END MAIN BLOCK */  
   
   

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