В первой части этой серии статей, я создал хорошую функцию сохранения, равно как и другую функцию, позволяющую создавать изменяемые документо-ориентированные таблицы на лету. Они работают исправно и делают именно то, что надо, но мы можем сделать еще многое. Особенно: я хочу полнотекстовый поиск, индексируемый на лету и сохранение многих документов внутри транзакции.
Полнотекстовый поиск
Наша документо-ориентированная таблица имеет поисковое поле типа tsvector, которое индексируется, используя GIN индекс для скорости. Я хочу обновлять это поле каждый раз, когда сохраняю документ, и не хочу много шума от API когда я это делаю.
В связи с этим, я прибегну к некоторой условности.
Обычно, при создании полнотекстового индекса, поля хранятся с довольно-таки специфическими именами. Такими как:
- Имя, или фамилия, возможно адрес электронной почты
- Название, или описание чего-то
- Адресная информация
Я хотел бы проверить мой документ в момент сохранения на наличие каких-либо ключей, которые я хотел бы проиндексировать и после этого сохранить их в поле search. Это возможно сделать при помощи функции, которую я назвал update_search:
create function update_search(tbl varchar, id int) returns boolean as $$ //get the record var found = plv8.execute("select body from " + tbl + " where id=$1",id)[0]; if(found){ var doc = JSON.parse(found.body); var searchFields = ["name","email","first","first_name", "last","last_name","description","title", "street", "city", "state", "zip", ]; var searchVals = []; for(var key in doc){ if(searchFields.indexOf(key.toLowerCase()) > -1){ searchVals.push(doc[key]); } }; if(searchVals.length > 0){ var updateSql = "update " + tbl + " set search = to_tsvector($1) where id =$2"; plv8.execute(updateSql, searchVals.join(" "), id); } return true; }else{ return false; } $$ language plv8;
Я вновь использую javascript (PLV8) для этих целей, и вытягиваю документ, основанный на ID. После чего я прохожу по всем ключам, что проверить, есть ли среди них те, которые я мог бы захотеть хранить, и, если есть, помещаю их в массив.
Если в этот архив есть попадания, я конкатинирую эти объекты и сохраняю их в поле search документа, используя функцию to_tsvector, являющуюся встроенной в Postgres, которая берет обычный текст и превращает его в индексируемые величины.
Вот и оно! Выполняя этот скрипт, мы получаем следующее:

Идеально — теперь я просто могу вставить это в конец моей save_document функции и оно будет вызываться транзакционно каждый раз, когда я что-то сохраняю:
create function save_document(tbl varchar, doc_string jsonb) returns jsonb as $$ var doc = JSON.parse(doc_string); var result = null; var id = doc.id; var exists = plv8.execute("select table_name from information_schema.tables where table_name = $1", tbl)[0]; if(!exists){ plv8.execute("select create_document_table('" + tbl + "');"); } if(id){ result = plv8.execute("update " + tbl + " set body=$1, updated_at = now() where id=$2 returning *;",doc_string,id); }else{ result = plv8.execute("insert into " + tbl + "(body) values($1) returning *;", doc_string); id = result[0].id; doc.id = id; result = plv8.execute("update " + tbl + " set body=$1 where id=$2 returning *",JSON.stringify(doc),id); } //run the search indexer plv8.execute("perform update_search($1, $2)", tbl,id); return result[0] ? result[0].body : null; $$ language plv8;
Сохранение многих документов
На данный момент, я могу передать единичный документ функции save_document, но я хотел бы иметь возможность передать ей массив. Я могу сделать это проверяя тип аргумента, после чего запускать цикл:
create function save_document(tbl varchar, doc_string jsonb) returns jsonb as $$ var doc = JSON.parse(doc_string); var exists = plv8.execute("select table_name from information_schema.tables where table_name = $1", tbl)[0]; if(!exists){ plv8.execute("select create_document_table('" + tbl + "');"); } //function that executes our SQL statement var executeSql = function(theDoc){ var result = null; var id = theDoc.id; var toSave = JSON.stringify(theDoc); if(id){ result=plv8.execute("update " + tbl + " set body=$1, updated_at = now() where id=$2 returning *;",toSave, id); }else{ result=plv8.execute("insert into " + tbl + "(body) values($1) returning *;", toSave); id = result[0].id; //put the id back on the document theDoc.id = id; //resave it result = plv8.execute("update " + tbl + " set body=$1 where id=$2 returning *;",JSON.stringify(theDoc),id); } plv8.execute("select update_search($1,$2)", tbl, id); return result ? result[0].body : null; } var out = null; //was an array passed in? if(doc instanceof Array){ for(var i = 0; i < doc.length;i++){ executeSql(doc[i]); } //just report back how many documents were saved out = JSON.stringify({count : i, success : true}); }else{ out = executeSql(doc); } return out; $$ language plv8;
Хорошая сторона работы здесь с javascript’ом заключается в том, что логика, необходимая для такой рутины, достаточно проста (в противоположность PLPGSQL). Я выделил весь процесс сохранения в его отдельную функцию — которая является javascript’ом после всего — таким образом я могу избежать дублирования.
Затем я хочу проверить, что подаваемый на вход аргумент — массив. Если это так, то я иду по его членам и вызываю executeSql, возвращая все, что накопилось при выполнении.
Если это не массив, я просто выполняю все так же, как и было, возвращая документ целиком. Результат:

Прекрасно! Лучшее в этом то, что это все происходит внутри транзакции. Мне это нравится!
Странности Node
Если бы только это могло работать идеально из Node! Я пробовал и в .NET, и в Node, с .NET все просто работает (странно) используя библиотеку Npgsql. Из Node, не то чтобы.
Короче говоря: node_pg драйвер делает очень странное преобразование, когда он видит объект массива в качестве входного параметра. Обратим внимание на следующее:
var pg = require("pg"); var run = function (sql, params, next) { pg.connect(args.connectionString, function (err, db, done) { //throw if there's a connection error assert.ok(err === null, err); db.query(sql, params, function (err, result) { //we have the results, release the connection done(); pg.end(); if(err){ next(err,null); }else{ next(null, result.rows); } }); }); }; run("select * from save_document($1, $2)", ['customer_docs', {name : "Larry"}], function(err,res){ //works just fine }
Это обычный Node/PG код. В самом конце, функция run настроена чтобы вызывать мою save_document функцию и передавать некоторые данные. Когда PG видит входной объект, он превращает его в строку и сохранение пройдет нормально.
В случае же, если послать массив…
run("select * from save_document($1, $2)", ['customer_docs', [{name : "Larry"}, {name : "Susie"}], function(err,res){ //crashes hard }
Я получаю обратно ошибку, сообщающую мне, что это некорректный JSON. Сообщение об ошибке (из Postgres) сообщит, что это связано со скудно отформатированным JSON:
{"{name : "Larry"}, ...}
Что… да, это ужасно. Я пытался сформулировать что происходит, но это просто-напросто выглядит так, что node_pg драйвер разбирает внешний массив — возможно вызывая метод flatten библиотеки Underscores. Я не знаю. Чтобы это обойти, вам необходимо изменить ваш вызов на следующее:
run("select * from save_document($1, $2)", ['customer_docs', JSON.stringify([{name : "Larry"}, {name : "Susie"}]), function(err,res){ //Works fine }
Вперед!
Процедура сохранения довольна гладкая и это меня радует. В следующей статье я настрою поисковики, а также создам функцию полнотекстового поиска.
ссылка на оригинал статьи http://habrahabr.ru/post/272411/
Добавить комментарий