Нормализация ФИАС и поиск с использованием словаря

Нормализация ФИАС и поиск с использованием словаря


Когда-то программировал .com (размер файла, напомню, <=64 kb). Сегодня ограничений почти нет, но если подсчитать количество мусорной информации, собранной на дисках всего мира и затраты на обслуживание… плюс биткоины… а хватит ли угля и газа на все это…


ФИАС – думаю, что аббревиатура знакома для большинства программистов, с рождения был монстром. В данный момент в архиве fias_dbf.rar 4.5 гигабайта, fias_xml.rar 6.3 гигабайта – заметим, что архив КЛАДР с почти аналогичной функциональностью порядка 30 мегабайт. При всех своих недостататках, (из них был всего лишь один существенный, а именно – неточность информации) КЛАДР решал свою задачу в достаточной степени хорошо. С 01.01.2018 КЛАДРу предписано not update.

В моих программа (налоговый учет и прочая бухгалтерская) КЛАДР есть везде. Год назад была предпринята попытка создания аналога КЛАДР из данных ФИАС, получилось неплохо, но из-за длительности update внедрение отложил. Интернет моих клиентов на бюджетном уровне — скорость скачивания от 3 до 100 кб, посему проще скачать «готовую» базу ФИАС.


Начнем с нормализации. Смотрим на «главную» таблицу ФИАС ADDROB.DBF – после преобразований она может принять подобную структуру (почти КЛАДР):


CREATE TABLE FIASD (
    ID           AS_INT /* AS_INT = INTEGER NOT NULL */,
    PID          AS_INT /* AS_INT = INTEGER NOT NULL */,
    OKTMO_ID     AS_INT /* AS_INT = INTEGER NOT NULL */,
    POSTCODE_ID  AS_INT /* AS_INT = INTEGER NOT NULL */,
    IFNS_ID      AS_INT /* AS_INT = INTEGER NOT NULL */,
    REG_ID       AS_INT /* AS_INT = INTEGER NOT NULL */,
    NAME_ID      AS_INT /* AS_INT = INTEGER NOT NULL */,
    SOCKR_ID     AS_INT /* AS_INT = INTEGER NOT NULL */
);
ALTER TABLE FIASD ADD CONSTRAINT PK_FIASD PRIMARY KEY (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_2 FOREIGN KEY (PID) REFERENCES GUIDS (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_3 FOREIGN KEY (OKTMO_ID) REFERENCES OKTMOS (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_4 FOREIGN KEY (POSTCODE_ID) REFERENCES POSTCODES (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_5 FOREIGN KEY (IFNS_ID) REFERENCES IFNS (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_6 FOREIGN KEY (REG_ID) REFERENCES REGIONS (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_8 FOREIGN KEY (NAME_ID) REFERENCES OFF_NAMES (ID);
ALTER TABLE FIASD ADD CONSTRAINT FK_FIASD_9 FOREIGN KEY (SOCKR_ID) REFERENCES SOCKRS (ID);



ID, PID – ссылки на GUIDS




Все остальные справочники аналоги GUIDS — идентификатор + значение


Вернемся к таблице ФИАС ADDROB.DBF. Видим FORMALNAME VARCHAR(120), OFFNAME VARCHAR(120). Первое поле предназначено для поиска, второе – для отображения результата, по сути и начинке одно и тоже, посему оставим OFFNAME в качестве образца для дальнейшего использования.
Таблица OFF_NAMES



Теперь несколько отойдем от темы — определимся с назначением ФИАС. Это поисковая система, т.е. после ввода в строку запроса «50 уфа» сервер должен выдать похожее вот на это ( «уфа 50» при этом вернет то же самое):





Поиск ведется в наименованиях адресных объектов, но как искать? Попробуем через словарь.
Таблица WORDSF:




Таблица OFF_NW:




Обе таблицы OFF_NW и WORDSF «пополняются» за счет триггеров на OFF_NAMES – наименование разбирается на слова с фильтром lower (ch>='а' and ch<='я') OR (ch>='0' and ch<='9'), проверяется на наличие в WORDSF – insert, если нет, проверяется наличие связи в OFF_NW. Результат ниже



Т.е. при запросе


   select off_names.name from wordsf
  left join off_nw on wordsf.id=off_nw.wordsf_id
  left join off_names on off_nw.off_name_id=off_names.id 
  where wordsf.aword='уфа' 

вернется




с производительностью




Усложним запрос


  select off_nw.off_name_id,
  sum(case when wordsf.aword starting with :ss1 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss2 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss3 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss4 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss5 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss6 then 1 else 0 end)
  from off_nw
  right join wordsf on off_nw.wordsf_id=wordsf.id
  where
  ((:ss1 is not null) and (wordsf.aword starting with :ss1)) or
  ((:ss2 is not null) and (wordsf.aword starting with :ss2)) or
  ((:ss3 is not null) and (wordsf.aword starting with :ss3)) or
  ((:ss4 is not null) and (wordsf.aword starting with :ss4)) or
  ((:ss5 is not null) and (wordsf.aword starting with :ss5)) or
  ((:ss6 is not null) and (wordsf.aword starting with :ss6))
  group by off_nw.off_name_id

где ss1=’50’,ss2=’лет’,ss3=’уфа’, иные = null. Смысл всего этого — получить список off_name_id с указанием количества вхождений поиска в этом наименовании.
Получим это




Видим, что OFF_NAME_ID=1295 (1459) ссылка на наименование объекта, содержащего два слова из запроса — «50» и «лет». Остается найти объекты выше уровнем, с наименование, содержащим «уфа».
Ниже «самолепный» запрос, реализующий означенный поиск


create or alter procedure LIST_FIAS (
    SS1 type of AS_STR10,
    SS2 type of AS_STR10,
    SS3 type of AS_STR10,
    SS4 type of AS_STR10,
    SS5 type of AS_STR10,
    SS6 type of AS_STR10)
returns (
    OBJ_CODE type of AS_STR36,
    FNAME type of AS_STR500,
    INFNS_CODE type of column IFNS.CODE,
    POST_CODE type of column POSTCODES.CODE,
    OKTMO_CODE type of column OKTMOS.CODE)
AS
declare variable id type of AS_INT;
declare variable NAME_ID type of AS_INT;
declare variable CSS1 type of AS_INT;
declare variable CSS2 type of AS_INT;
declare variable CSS3 type of AS_INT;
declare variable CSS4 type of AS_INT;
declare variable CSS5 type of AS_INT;
declare variable CSS6 type of AS_INT;
declare variable S1 type of AS_STR10;
declare variable S2 type of AS_STR10;
declare variable S3 type of AS_STR10;
declare variable S4 type of AS_STR10;
declare variable S5 type of AS_STR10;
declare variable S6 type of AS_STR10;
declare variable pS1 type of AS_STR10;
declare variable pS2 type of AS_STR10;
declare variable pS3 type of AS_STR10;
declare variable pS4 type of AS_STR10;
declare variable pS5 type of AS_STR10;
declare variable pS6 type of AS_STR10;

declare variable lpid type of as_int;
declare variable wcount type of as_int;
declare variable wpcount type of as_int;
declare variable lwcount type of as_int;
declare variable rz type of as_int;

BEGIN
  wcount=iif(ss1 is null,0,1)+iif(ss2 is null,0,1)+iif(ss3 is null,0,1)+
  iif(ss4 is null,0,1)+iif(ss5 is null,0,1)+iif(ss6 is null,0,1);
-- wcount – кол-во «слов» в запросе – не более 6
  for select off_nw.off_name_id, -- group by 
-- sum(case when – счетчик вхождений 
  sum(case when wordsf.aword starting with :ss1 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss2 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss3 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss4 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss5 then 1 else 0 end),
  sum(case when wordsf.aword starting with :ss6 then 1 else 0 end)
  from off_nw
  right join wordsf on off_nw.wordsf_id=wordsf.id
  where
  ((:ss1 is not null) and (wordsf.aword starting with :ss1)) or
  ((:ss2 is not null) and (wordsf.aword starting with :ss2)) or
  ((:ss3 is not null) and (wordsf.aword starting with :ss3)) or
  ((:ss4 is not null) and (wordsf.aword starting with :ss4)) or
  ((:ss5 is not null) and (wordsf.aword starting with :ss5)) or
  ((:ss6 is not null) and (wordsf.aword starting with :ss6))
  group by off_nw.off_name_id
  into :name_id,:css1,:css2,:css3,:css4,:css5,:css6 do
  begin
    lwcount=css1+css2+css3+css4+css5+css6; -- найдено слов
    ps1=iif(css1=0,ss1,null); -- заполняем «массив» не найденных слов
    ps2=iif(css2=0,ss2,null);
    ps3=iif(css3=0,ss3,null);
    ps4=iif(css4=0,ss4,null);
    ps5=iif(css5=0,ss5,null);
    ps6=iif(css6=0,ss6,null);
    -----------------------------------------
    for select fiasd.id, fiasd.pid
    from fiasd where fiasd.name_id=:name_id
    into :id,:lpid do
    begin
      fname=(select fname from ret_obj_adr(:id));
      if (wcount=lwcount) then – если все слова найдены
      begin
        fname=(select fname from ret_footer_adr(:lpid))||', '||fname;
        select postcodes.code,ifns.code,oktmos.code,guids.aguid from fiasd
        left join postcodes on fiasd.postcode_id=postcodes.id
        left join ifns on fiasd.ifns_id=ifns.id
        left join oktmos on fiasd.oktmo_id=oktmos.id
        left join guids on fiasd.id=guids.id where fiasd.id=:id into
        :post_code,:infns_code,:oktmo_code,:obj_code;
        suspend;
      end else
      begin
        while (lpid>0) do – пока не будут найдены все слова
        begin
          s1=iif(css1=0,ps1,null); -- заполняем аргументы из «массива»
          s2=iif(css2=0,ps2,null);
          s3=iif(css3=0,ps3,null);
          s4=iif(css4=0,ps4,null);
          s5=iif(css5=0,ps5,null);
          s6=iif(css6=0,ps6,null);
          wpcount=iif(s1 is null,0,1)+iif(s2 is null,0,1)+iif(s3 is null,0,1)+
          iif(s4 is null,0,1)+iif(s5 is null,0,1)+iif(s6 is null,0,1); -- устанавливаеи счетчик
          select rz,css1,css2,css3,css4,css5,css6
          from test_word_in_pid(:lpid,  -- здесь аналогичная процедура поиска «выше» 
          :s1,:s2,:s3,:s4,:s5,:s6)
          into :rz,:css1,:css2,:css3,:css4,:css5,:css6;
          lwcount=css1+css2+css3+css4+css5+css6;
          if (rz=1) then
          begin
            if (wpcount=lwcount) then
            begin
              fname=(select fname from ret_footer_adr(:lpid))||', '||fname;
              select postcodes.code,ifns.code,oktmos.code,guids.aguid from fiasd
              left join postcodes on fiasd.postcode_id=postcodes.id
              left join ifns on fiasd.ifns_id=ifns.id
              left join oktmos on fiasd.oktmo_id=oktmos.id
              left join guids on fiasd.id=guids.id where fiasd.id=:id into
              :post_code,:infns_code,:oktmo_code,:obj_code;
              suspend;
              lpid=0;
            end else
            begin
              fname=(select fname from ret_footer_adr(:lpid))||', '||fname;
              select pid from fiasd where id=:lpid into :lpid;
            end
          end else
          begin
            fname=(select fname from ret_footer_adr(:lpid))||', '||fname;
            select pid from fiasd where id=:lpid into :lpid;
          end
        end
      end
    end
  end
end


Результат




109ms – это явно не тяжелый запрос.
Как заключение – описанный метод поиска можно оптимизировать за счет лучшего владения SQL – мой изложенный пока на уровне «так себе» в силу того, что иногда я просто не понимаю свой усложненный код буквально через месяц простоя и приучил себя писать код «как для всех». Но не всегда получается и приходится начинать все снова (заново) с надеждой, что успею дописать… пока не забуду

Метки:
ФИАС