Язык запросов
12.4. Синтаксический разбор запросов
Настоящий параграф посвящен разработке языка запросов к базе данных, близкого к естественному языку. В запросах представлены операции объединения, пересечения и разности множеств (найти сыновей Петра и дочерей мужчин), композиции отношений (найти сыновей предков Петра) и отрицания (найти сыновей Петра, но не сыновей Марии). Объединение представляют союзы "и" и "или", пересечение — знак запятой "," (поэтому его нельзя использовать просто как знак пунктуации), а отрицание — частица "не". Все запросы формулируются через набор основных отношений, которыми являются бинарные отношения "родитель", "ребенок", "отец", "мать", "муж", "жена", "сын", "дочь", "сестра", "брат", "предок" и "потомок", а также унарные отношения "мужчина" и "женщина". Например, найти тетушек Петра можно с помощью запроса "найти сестер родителей Петра". Без аргумента в запросе могут быть только унарные отношения. Например, нельзя попросить "найти отцов", но можно "найти отцов мужчин и отцов женщин".
Язык запросов удовлетворяет следующей грамматике:
query ::= conj conjs conj ::= elem elems elem ::= rel elem | [не] rel elem | name surname | name | unrel elems ::= consign elem elems | none conjs ::= dizsign conj conjs | none consign ::= [,] dizsign ::= [и] | [или] unrel ::= [мужчина] | [женщина] % и изменения по падежам и числам rel ::= [родитель] | [предок] | … % и изменения по падежам и числам
Запросы могут быть вида:
Найти братьев мужчин и сына Петра;
Найти предков потомков предков дочерей Петра Иванова или Марию.
Найти предков Петра, но не сыновей отца отца Петра.
Программа удаляет из запроса слова, не значимые при вычислении ответа на запрос, т. е. "игнорируемые" слова. Например, из приведенных выше запросов удаляются слова "найти" и "но".
Имена и фамилии людей, а также названия отношений изменяются по падежам и числам. Поэтому при обработке запроса выполняется операция нормализации таких слов. Для отношений проверяется, является ли неизменяемая часть слова префиксом названия отношения в запросе. Например, если слово в запросе имеет префикс "сест", то оно распознается как название отношения "сестра". Отношение "ребенок" могут представлять слова как с префиксом "ребен", так и с префиксом "дет" (см. определение отношения rel в программе).
Имена обрабатываются следующим образом. Сначала проверяется, имеется ли такое имя в базе данных (с учетом регистра). Если нет, то отнимается один символ с конца слова и проверяется, является ли полученное слово префиксом некоторого имени. Затем, при необходимости, отнимается еще один символ, и т. д. Фамилии распознаются аналогичным образом, в паре с именами.
Например, запрос
Найти сестер Анны и сыновей Петра Иванова
преобразуется в терм
diz(rel("сестра", n("Анна")), rel("сын", ns("Петр", "Иванов")))
Ниже приведен класс parser с интерфейсом parser (листинги 12.28–12.30).
domains term = diz(term, term); con(term, term); neg(term); rel(string, term); unrel(string); n(string); ns(string, string). predicates parse: (string) -> term.Пример 12.28. Интерфейс parser
constructors new: (dbrel).Пример 12.29. Декларация класса parser
facts db: dbrel. clauses new(Db):- db := Db. parse(Str) = Term:- L = scan(string::toLowerCase(Str)), L1 = list::filter(L, {(S):- not(list::isMember(S, ignor))}), parser(query, L1, Term, Rest), !, write(Rest), nl. parse(_) = n(""). predicates scan: (string) -> string*. clauses scan(Str) = [Tok | scan(RestStr)]:- string::frontToken(Str, Tok, RestStr), !. scan(_) = []. facts ignor : string* := ["найти", "вычислить", "но", "которые", "являются", "а", "также"]. domains nt = query; conj; conjs; elem; elems. predicates parser: (nt, string*, term [out], string* [out]) determ. parser: (nt, string*, term, term [out], string* [out]). clauses parser(query, L, Term, Rest):- parser(conj, L, Term1, L1), parser(conjs, L1, Term1, Term, Rest). parser(conj, L, Term, Rest):- parser(elem, L, Term1, L1), parser(elems, L1, Term1, Term, Rest). parser(elem, ["не" | L], neg(Term), Rest):- parser(elem, L, Term, Rest), !. parser(elem, [Rel | L], unrel(Rel1), L):- Rel1 = norm("ur", Rel), !. parser(elem, [Rel | L], rel(Rel1, Term), Rest):- Rel1 = norm("r", Rel), !, parser(elem, L, Term, Rest). parser(elem, [Name, Surname | L], ns(Name1, Surname1), L):- norm(Name, Surname, Name1, Surname1), !. parser(elem, [Name | L], n(Name1), L):- Name1 = norm("n", Name). parser(conjs, [S | L], Term1, Term, Rest):- dizsign(S), parser(conj, L, Term2, L1), !, parser(conjs, L1, diz(Term1, Term2), Term, Rest). parser(elems, [S | L], Term1, Term, Rest):- consign(S), parser(elem, L, Term2, L1), !, parser(elems, L1, con(Term1, Term2), Term, Rest). parser(_, L, Term, Term, L). facts consign: (string). dizsign: (string). rel: (string, string). urel: (string, string). clauses consign(","). dizsign("и"). dizsign("или"). rel("родитель", "родител"). rel("ребенок", "ребен"). rel("ребенок", "дет"). rel("отец", "отц"). rel("отец", "отец"). rel("мать", "мат"). rel("муж", "муж"). rel("жена", "жен"). rel("сын", "сын"). rel("дочь", "доч"). rel("сестра", "сест"). rel("брат", "брат"). rel("предок", "пред"). rel("потомок", "потом"). urel(dbrel::male, "мужчин"). urel(dbrel::female, "женщин"). predicates norm: (string, string) -> string determ. norm: (string, string, string [out], string [out]) determ. getPrefix_nd: (string) -> string nondeterm. clauses getPrefix_nd(S) = S. getPrefix_nd(S) = getPrefix_nd(Prefix):- L = string::length(S), L > 2, string::front(S, L - 1, Prefix, _). norm("r", S) = NormS:- rel(NormS, Sub), string::hasPrefix(S, Sub, _), !. norm("ur", S) = NormS:- urel(NormS, Sub), string::hasPrefix(S, Sub, _), !. norm("n", S) = Name:- db:person_nd(_, Name, _, _, _, _), Name1 = string::toLowerCase(Name), S1 = getPrefix_nd(S), string::hasPrefix(Name1, S1, _), !. norm(N, S, Name, Surname):- db:person_nd(_, Name, Surname, _, _, _), Name1 = string::toLowerCase(Name), SName1 = string::toLowerCase(Surname), N1 = getPrefix_nd(N), string::hasPrefix(Name1, N1, _), S1 = getPrefix_nd(S), string::hasPrefix(SName1, S1, _), !.Пример 12.30. Имплементация класса parser
Предикат front возвращает префикс строки, состоящий из заданного количества символов, и остаток строки. Предикат hasPrefix проверяет, является ли заданная подстрока префиксом строки и возвращает остаток строки.
12.5. Вычисление ответов на запросы
В настоящем параграфе реализуется процедура поиска ответов на вопросы. Все вычисления проводятся на множестве идентификаторов. Результом вычисления запроса является список идентификаторов. Из списков удаляются повторяющиеся элементы. К спискам применяются операции объединения, пересечения и разности.
Для реализации вычислений создается класс calculation с интерфейсом calculation (листинги 12.31–12.33).
predicates calc: (parser::term) -> unsigned* determ.Пример 12.31. Интерфейс calculation
constructors new: (relation).Пример 12.32. Декларация класса calculation
open core, parser, list facts rel: relation. clauses new(R):- rel := R. predicates n: (A*) -> A*. calc1: (term) -> unsigned nondeterm. clauses n(L) = removeDuplicates(L). calc1(X) = getMember_nd(calc(X)). calc(diz(X, Y)) = union(calc(X), calc(Y)). calc(con(X, Y)) = intersection(calc(X), calc(Y)). calc(neg(X)) = difference(L, calc(X)):- L = [I || rel:db:person_nd(I, _, _, _, _, _)]. calc(parser::n(N)) = [I || rel:db:person_nd(I, N, _, _, _, _)]. calc(ns(N, S)) = [I || rel:db:person_nd(I, N, S, _, _, _)]. calc(unrel(Sex)) = [I || rel:db:person_nd(I, _, _, Sex, _, _)]. calc(rel("родитель", X)) = n([I || rel:parent(I, calc1(X))]). calc(rel("ребенок", X)) = n([I || rel:parent(calc1(X), I)]). calc(rel("отец", X)) = n([I || rel:father(I, calc1(X))]). calc(rel("мать", X)) = n([I || rel:mother(I, calc1(X))]). calc(rel("муж", X)) = n([I || rel:husband(I, calc1(X))]). calc(rel("жена", X)) = n([I || rel:husband(calc1(X), I)]). calc(rel("сын", X)) = n([I || rel:son(I, calc1(X))]). calc(rel("дочь", X)) = n([I || rel:daughter(I, calc1(X))]). calc(rel("сестра", X)) = n([I || rel:sister(I, calc1(X))]). calc(rel("брат", X)) = n([I || rel:brother(I, calc1(X))]). calc(rel("предок", X)) = n([I || rel:ancestor(I, calc1(X))]). calc(rel("потомок", X)) = n([I || rel:ancestor(calc1(X), I)]).Пример 12.33. Имплементация класса calculation
По списку идентификаторов восстанавливаются имена и фамилии людей, которые и выдаются в качестве ответа на запрос.
В упражнениях 4 – 8 (см. ниже) требуется создать базу данных, а также придумать и реализовать язык запросов к ней, близкий к естественному языку.
Упражнения
- Добавьте в базу данных сведения о годах жизни людей. Добавьте запросы, связанные с годами жизни.
- Добавьте в базу данных и в язык запросов отношения свойства.
- Добавьте в базу данных и в язык запросов сведения о роде занятий и о месте жительства людей.
- База данных "Династия" (Романовых, Рюриковичей или др.).
- База данных "География России", описывающая взаимоотношения между объектами некоторой области.
- База данных "Биология", описывающая взаимоотношения между растениями некоторого семейства.
- База данных "Естественные языки", описывающая взаимоотношения между языками в некоторой группе языков.
- База данных "Языки программирования", описывающая взаимоотношения между языками в некоторой парадигме.