Начинаем работать с графовой базой данных Neo4j

В нашем проекте возникла следующая задача — есть база с большим количеством товаров, на уровне сотен тысяч. У каждого товара есть сотни динамически создаваемых характеристик. Необходимо обеспечить быструю фильтрацию по товарам по набору различных характеристик. Время формирования ответа должно быть не более 0.3 секунды, нужно поддерживать сложную логику в стиле.

(характеристика1 = true AND (характеристика2 < 100)) OR (характеристика1 = false AND (характеристика3 > 17)) ... далее обычно мешанина из AND\OR


Типичный пример подобного функционала — hotline.ua/computer/myshi-klaviatury

Пример функционала

У нас все реализовано в рамках MySQL + Symfony2/Doctrine, скорость неудовлетворительная — ответы формируются в течении 1-10 секунд. Мои попытки оптимизировать все это хозяйство — под катом.


Терминология задачи по фильтрации товаров (в упрощенном виде)


  • характеристика — определенное свойство товара. Например, объем памяти.
  • шаблон товара — набор всех возможных характеристик однотипных товаров, например — перечень возможных характеристик компьютерных мышек. При добавлении нового товара администратор может выбирать характеристики в рамках шаблона. Добавить новую характеристику для одного товара невозможно — нужно добавить характеристику в шаблон для этого товара. Одновременно эта характеристика будет доступна для всех товаров, использующих этот шаблон
  • группа товаров — товары на основе одного шаблона. Например, компьютерные мышки. Фильтрация делается только для товаров из одной группы
  • критерий — логическое правило, которое состоит из набора формальных требований к характеристикам товара. Например, «геймерская мышка» — это набор требований к характеристикам (размер не миниатюрный) AND (сенсор лазерный) AND (разрешение сенсора не менее 1500)
  • фильтр — группа критериев для однотипных товаров. В зависимости от критериев, они могут комбинироваться через AND или OR

У hotline реализован более продвинутый вариант — с подсказкой, сколько товаров останется после активации критерия. Например, если выбрать фильтр «Bluetooth», то после загрузки страницы возле фильтра «Тип сенсора мыши — оптический» будет цифра 17. Фактически, для такой реализации нужно не просто делать выборку по критериям, но и предварительно для каждого оставшегося фильтра подсчитывать количество товаров при его активации.

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

Терминология Neo4j и графовых баз данных в целом.


  • graph database, графовая база данных — база данных построенная на графах — узлах и связях между ними
  • Cypher — язык для написания запросов к базе данных Neo4j (примерно, как SQL в MYSQL)
  • node, нода — объект в базе данных, узел графа. Количество узлов ограниченно 2 в степени 35 ~ 34 биллиона
  • node label, метка ноды — используется как условный «тип ноды». Например, ноды типа movie могут быть связанны с нодами типа actor. Метки нод — регистрозависимые, причем Cypher не выдает ошибок, если набрать не в том регистре название.
  • relation, связь — связь между двумя нодами, ребро графа. Количество связей ограниченно 2 в степени 35 ~ 34 биллиона
  • relation identirfier, тип связи — в Neo4j у связей. Максимальное количество типов связей 32767
  • properties, свойства ноды — набор данных, которые можно назначить ноде. Например, если нода — это товар, то в свойствах ноды можно хранить id товара из базы MySQL
  • node ID, ID нода — уникальный идентификатор ноды. По умолчанию, при просмотрах результата отображается именно этот ID. как его использовать в Cypher запросах я не нашел


Схема решения задачи


Для каждого товара создать отдельную ноду, в свойствах ноды хранить id товара в базе MySQL. Для каждого критерия создать свою ноду, в свойствах хранить id критерия. Дальше, связать все ноды товаров с нодами критериев, которые подходят для товара. При изменении характеристик товара или свойств критериев обновлять связи между нодами.

Первый вариант решения — с Neo4j


Учитывая, что я с графовыми базами данных никогда не работал — я решил развернуть локально Neo4j, изучить на базовом уровне Cypher и попробовать реализовать требуемую логику. Если все получиться — провести тестирование скорости работы для базы из 1 миллиона товаров, у каждого 500 характеристик.

Разворачивание системы достаточно простое — скачиваем дистрибутив и устанавливаем его.

У Neo4j сервера есть RestAPI, для php есть библиотека neo4jphp. Также есть bundle для интеграции с Symfony2 — klaussilveira/neo4j-ogm-bundle.

В дистрибутив входит веб сервер и приложение для работы с ним, по умолчанию http://localhost:7474/
Есть еще старая версия клиента, с другим функционалом.

В качестве документации удобно использовать краткую документацию. Примеры кода есть в graphgist. По идее, они должны там выполнятся онлайн, но сейчас это не работает. Чтобы посмотреть код нужно перейти по ссылке из graphgist (например, сюда) и там нажать кнопку Page Source.

Для экспериментов с Neo4j очень удобно использовать встроенный веб клиент Там можно выполнять запросы Cypher и просматривать ответ на запросы вместе со связями и характеристиками нод.

Встроенный клиент Node4j

Простые Cypher команды

Создание ноды с меткой
create (n:Ware {wareId: 1}); 

Выбрать все ноды
MATCH (n) RETURN n;

Счетчик
MATCH (n:Ware {wareId:1}) RETURN "Our graph have "+count(*)+" Nodes with label Ware and wareId=1" as counter;

Создать 2 связанные ноды
CREATE (n{wareId:1})-[r:SUIT]->(m{criteriaId:1})

Связать 2 существующие ноды
MATCH (a {wareId: 1}),
      (b {criteriaId: 2})
MERGE (a)-[r:SUIT]->(b)

Удалить все связанные ноды
match (n)-[r]-() DELETE n,r;

Удалить все несвязанные ноды — если попробовать запустить эту команду в базе, где есть связанные ноды — она не пройдет. Нужно удалить вначале связанные ноды.
match n DELETE n;

Выбрать товары, которым подходит критерий 3
MATCH (a:Ware)-->(b:Criteria {criteriaId: 3}) RETURN a;

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

Можно выполнить создание множества нод со связями одной командой, нужно давать разные имена нодам, связям можно не давать имя
CREATE (w1:Ware{wareId:1})-[:SUIT]->(c1:Criteria{criteriaId:1}), (w2:Ware{wareId:2})-[:SUIT]->(c2:Criteria{criteriaId:2}),  (w3:Ware{wareId:3})-[:SUIT]->(c3:Criteria{criteriaId:3}), (w4:Ware{wareId:4})-[:SUIT]->(c1), (w5:Ware{wareId:5})-[:SUIT]->(c1),  (w4)-[:SUIT]->(c2), (w5)-[:SUIT]->(c3);

Получится такая структура. Если у вас выглядит менее понятно — можно переставить мышкой ноды.

Тестовая структура

Промежуточные тесты скорости Neo4j


Пришло время протестировать скорость заполнения базы и простых выборок из большой базой.

Для этого клонируем neo4jphp
git clone https://github.com/jadell/neo4jphp.git

Базовое описание этой библиотеки есть в этом посте, поэтому я сразу выложу код для заполнения тестовой базы еxamples/test_fill_1.php
<?php
use Everyman\Neo4j\Client,
	Everyman\Neo4j\Index\NodeIndex,
	Everyman\Neo4j\Relationship,
	Everyman\Neo4j\Node,
	Everyman\Neo4j\Cypher;

require_once 'example_bootstrap.php';

$neoClient = new Client();
$neoWares = new NodeIndex($neoClient, 'Ware');
$neoCriterias = new NodeIndex($neoClient, 'Criteria');
$neoWareLabel = $neoClient->makeLabel('Ware');
$neoCriteriaLabel = $neoClient->makeLabel('Criteria');

$wareTemplatesCount = 200; // количество шаблонов товара
$criteriasCount = 500;       // количество критериев
$waresCount = 10000;     // количество товаров
$commitWares = 100;      // количество товаров, которое будет идти в 1 batch
$minRelations = 200;      // минимальное количество связей товара с критериями
$maxRelations = 400;     // максимальное количество связей товара с критериями

$time = time();
for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) {
	$neoClient->startBatch();
	print $wareTemplateId." (".$criteriasCount." criterias, ".$waresCount." wares with rand(".$minRelations.",".$maxRelations.") ...";
	$criterias = array();
        // создаем критерии
	for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) {
		$c = $neoClient->makeNode()->setProperty('criteriaId', $wareTemplateId * $criteriasCount + $criteriaId)->save(); // ->addLabels(array($neoCriteriaLabel)) - не работает с commitBatch
		$neoCriterias->add($c, 'criteriaId', $wareTemplateId * $wareTemplatesCount + $criteriaId); // ->save() такого метода нет
		$criterias[] = $c;
	}
        // создаем товары
	for($wareId = 1;$wareId <=$waresCount;$wareId++) {
		$w = $neoClient->makeNode()->setProperty('wareId', $wareTemplateId * $waresCount + $wareId)->save(); // ->addLabels(array($neoWareLabel)) - не работает с commitBatch
		$neoWares->add($c, 'wareId', $wareTemplateId * $waresCount + $criteriaId);

		// каждый товар привязываем к случайному количеству критериев
		for($i = 1;$i<=rand($minRelations,$maxRelations);$i++) {
			$w->relateTo($criterias[array_rand($criterias)], "SUIT")->save();
		}
		if(($wareId % $commitWares) == 0) {
                        // комитим, при слишком больших комитах Neo4j зависает
			$neoClient->commitBatch();
		        print " [commit ".$commitWares." ".(time() - $time)." sec]";
			$time = time();
			$neoClient->startBatch();
		}
	}
	$neoClient->commitBatch();
        print " done in ".(time() - $time)." seconds\n";
	$time = time();

}


Скрипт заполнения базы я оставил на ночь. Примерно спустя 4 часа скрипт перестал добавлять данные и сервис Neo4j начал грузить сервер на 100%. Утром по итогу работы было вставлено 78300 товаров из 8 категорий товаров.
Результаты тестового заполнения базы — примерно 20 товаров в секунду с 200-400 связями. Не очень высокий результат — Mysql и Cassandra выдавали около 10-20 тысяч вставок в секунду (10 полей, 1 primary index, 1 индекс). Но скорость вставки для нас не критична — мы можем обновлять граф данных в фоновом режиме после редактирования товара. А вот скорость выборки данных — критична.

Размер тестовой базы данных на диске — 1781 мегабайт. В ней храниться 78300 товаров, 4000 критериев, 15660000-31320000 связей. Общее количество объектов (нодов и связей) менее 32 миллионов — в среднем по 55 байт на сущность. Многовато, как по мне, но главное требование все же скорость выборок, а не размер базы.

Первая попытка протестировать скорость выборки провалилась — сервер Neo4j опять «ушел» в режим 100% загрузки процессора и за несколько минут так и не выдал ответ на запрос.
MATCH (c {criteriaId: 1})<--(a)-->(b {criteriaId: 3}) RETURN a.wareId;

Чтобы двигаться дальше нужно разобраться, как оптимизировать запрос в Neo4j. Вначале я хотел ограничить стартовый набор нод в выборке с помощью инструкции START
START n=node:nodeIndexName(key={value}) MATCH (c)<--(a)-->(b) RETURN a.wareId;

Для этого нужно, чтобы в базе были индексы. В Neo4j я не нашел команду для просмотра перечня текущих индексов, но в веб приложении Neo4j можно набрать команду
:schema

Добавить индексы можно командой
CREATE INDEX ON :Criteria(criteriaId)

Уникальный индекс можно создать командой
CREATE CONSTRAINT ON (n:Criteria) ASSERT n.criteriaId IS UNIQUE;

Индексы, добавленные командами выше, нельзя использовать в START директиве. Тут утверждают, что их можно использовать только в where
The indexes created via Cypher are called Schema indexes, and are not to be used in the START clause. The START clause index lookups are reserved for the legacy indexes that you create via autoindexing or through the non-Cypher APIs.

In order to use the :user index you've created, you can do this:

match n:user
where n.name=«aapo»
return n;

Если я правильно понял документацию, можно смело использовать WHERE вместо START
START is optional. If you do not specify explicit starting points, Cypher will try and infer starting points from your query. This is done based on node labels and predicates contained in your query. See Chapter 14, Schema for more information. In general, the START clause is only really needed when using legacy indexes.

Так родился первый рабочий запрос
MATCH (a:Ware)-->(c1:Criteria {criteriaId: 3}),(c2:Criteria {criteriaId: 1}),(c3:Criteria {criteriaId: 2}) WHERE (a)-->(c2) AND (a)-->(c3) RETURN a;

В нашей тестовой базе индексов не обнаружено, поэтому мы создадим еще одну базу для теста другим способом. Возможности создать независимые наборы данных (аналог базы данных в MySQL) в Neo4j я не нашел. Поэтому для тестирования я просто менял путь к хранилищу данных в настройках Neo4j Community (Database location)

Use в Neo4j делается сменой пути к хранилищу

Внимательные читатели возможно обнаружили пару комментариев в коде test_fill_1.php, а именно
		$c = $neoClient->makeNode()->setProperty('criteriaId', $wareTemplateId * $criteriasCount + $criteriaId)->save(); // ->addLabels(array($neoCriteriaLabel)) - не работает с commitBatch
		$neoCriterias->add($c, 'criteriaId', $wareTemplateId * $wareTemplatesCount + $criteriaId); // ->save() такого метода нет

В batch режиме в Neo4jphp у меня не получилось добавить метки к нодам, а индексы почему то не сохранились. Учитывая, что Cypher перестал для меня быть китайской грамотой, я решил заполнять базу хардкорно — на чистом Cypher. Так получился test_fill_2.php
<?php
use Everyman\Neo4j\Client,
	Everyman\Neo4j\Index\NodeIndex,
	Everyman\Neo4j\Relationship,
	Everyman\Neo4j\Node,
	Everyman\Neo4j\Cypher;

require_once 'example_bootstrap.php';

$neoClient = new Client();

$wareTemplatesCount = 100; // количество шаблонов товара
$criteriasCount = 50;       // количество критериев
$waresCount = 250;     // количество товаров
$minRelations = 20;      // минимальное количество связей товара с критериями
$maxRelations = 40;     // максимальное количество связей товара с критериями

if($maxRelations > $criteriasCount) {
    throw new \Exception("maxRelations[".$maxRelations."] should be bigger, that criteriasCount[".$criteriasCount."]");

}
$query = new Cypher\Query($neoClient, "CREATE CONSTRAINT ON (n:Criteria) ASSERT n.criteriaId IS UNIQUE;", array());
$result = $query->getResultSet();
$query = new Cypher\Query($neoClient, "CREATE CONSTRAINT ON (n:Ware) ASSERT n.wareId IS UNIQUE;", array());
$result = $query->getResultSet();

for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) {
    $time = time();
	$queryTemplate = "CREATE ";
	print $wareTemplateId." (".$criteriasCount." criterias, ".$waresCount." wares with rand(".$minRelations.",".$maxRelations.") ...";
	$criterias = array();
	for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) {
                // создаем нод критерия в виде (w1:Ware{wareId:1})
		$cId = $criteriaId + $criteriasCount*$wareTemplateId;
		$queryTemplate .= "(c".$cId.":Criteria{criteriaId:".$cId."}), ";
		$criterias[] = $cId;
	}

	for($wareId = 1;$wareId <=$waresCount;$wareId++) {
		$wId = $wareId + $waresCount*$wareTemplateId;
                // создаем нод товара в виде (w1:Ware{wareId:1})
		$queryTemplate .= "(w".$wId.":Ware{wareId:".$wId."}), ";

                // создаем связи между нодами в виде (w1)-[:SUIT]->(c1)
		$possibleLinks = array_merge(array(), $criterias); // clone $criterias не работает
		for($i = 1;$i<=rand($minRelations,$maxRelations);$i++) {
			$linkId = $possibleLinks[array_rand($possibleLinks)];
			unset($possibleLinks[$linkId]);
			$queryTemplate .= "w".$wId."-[:SUIT]->c".$linkId.", ";
		}
	}
    $queryTemplate = substr($queryTemplate,0,-2); // удаляем последний ", "

    $build = time();
	$query = new Cypher\Query($neoClient, $queryTemplate, array()); // $queryTemplate будет в районе 42 мегабайт для 10000 товаров, 500 критериев, 200-400 связей между товаром-критерием
	$result = $query->getResultSet();
        print " Query build in ".($build - $time)." seconds, executed in ".(time() - $build)." seconds\n";
//	die();
}

Скорость добавления данных оказалась предсказуемо большей, чем в первом варианте.
Тестовый скрипт с добавлением 30000 нодов и 500000 — 1000000 связей на cypher отработал за 140 секунд, база заняла на диске 62 мегабайта. При попытке запустить скрипт c $waresCount=1000 (не говоря уже о 10000 товаров) я получил ошибку «Stack overflow error». Я переписал скрипт c использованием.
MATCH (a {wareId: 1}),
      (b {criteriaId: 2})
MERGE (a)-[r:SUIT]->(b)

Это привело к катастрофическому падению скорости работы, модифицированный скрипт работал примерно около часа. Я решил протестировать скорость выборки по нескольким критериям и вернуться к вопросу быстрой вставки данных позже.
<?php
use Everyman\Neo4j\Client,
	Everyman\Neo4j\Index\NodeIndex,
	Everyman\Neo4j\Relationship,
	Everyman\Neo4j\Node,
	Everyman\Neo4j\Cypher;

require_once 'example_bootstrap.php';

$neoClient = new Client();
$time = microtime();
$query = new Cypher\Query($neoClient, "MATCH (a:Ware)-->(b:Criteria {criteriaId: 3}),(c:Criteria {criteriaId: 1}),(c2:Criteria {criteriaId: 2}) WHERE (a)-->(c) AND (a)-->(c2) RETURN a;", array());
$result = $query->getResultSet();
print "Done in ".(microtime() - $time)." seconds\n";

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

Альтернативное решение


Я решил «для очистки совести» опробовать MySQL в качестве хранилища. Связи между нодами будут храниться в отдельной таблице без дополнительной информации.

CREATE TABLE IF NOT EXISTS `edges` (
  `criteriaId` int(11) NOT NULL,
  `wareId` int(11) NOT NULL,
  UNIQUE KEY `criteriaId` (`criteriaId`,`wareId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Тестовый скрипт для заполнения базы ниже

<?php
mysql_connect("localhost", "root", "");
mysql_select_db("test_nodes");

$wareTemplatesCount = 100;
$criteriasCount = 50;
$waresCount = 250;
$minRelations = 20;
$maxRelations = 40;

$time = time();
for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) {
	$criterias = array();
	for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) {
		$criterias[] = $wareTemplateId * $criteriasCount + $criteriaId;
	}

	for($wareId = 1;$wareId <=$waresCount;$wareId++) {
        $edges = array();
		$wId = $wareTemplateId * $waresCount + $wareId;
        $links = array_rand($criterias,rand($minRelations,$maxRelations));
		foreach($links as $linkId) {
			$edges[] = "(".$criterias[$linkId].",".$wareId.")";
		}
                
                // заносим сразу связи между товарами и критериями
		mysql_query("INSERT INTO edges VALUES ".implode(",",$edges));
	}
	print ".";
}
print " [added ".$wareTemplatesCount." templates in ".(time() - $time)." sec]";
$time = time();


Заполнение базы заняло 12 секунд. Размер таблицы — 37 мегабайт. Поиск по 2 критериям занимает 0.0007 секунд

SELECT e1.wareId
FROM  `edges` AS e1
JOIN edges AS e2 ON e1.wareId = e2.wareId
WHERE e1.criteriaId =17
AND e2.criteriaId =31


Еще один вариант


Под mysql есть полноценное графовое хранилище данных — но я его не тестировал. Судя по документации, он гораздо примитивнее Neo4j.

Выводы


Neo4j — очень крутая штука. Запрос наподобие «Выбрать контакты пользователей, которые лайкнули киноактерам, которые снялись в фильмах, в которых звучали саунтдтреки, которые были написаны музыкантами, которым я поставил лайк» в Neo4j решается тривиально. Примерно так
MATCH (me:User {userId:123})-[:Like]->(musicants:User)-[:Author]->(s:Soundtrack)-[:Used]->(f:Film)<-[:Starred]-(actor: User)<-[:Like]-(u:User) RETURN u

Для SQL это гораздо более хлопотное занятие.

Сравнивать полноценную графовую базу с голой таблицей индексов в MySQL — некорректно, но в рамках решения моей задачи — использование Neo4j никаких плюсов не дало.

UPDATE. Изменил url'ы картинок, по идее должны у всех загрузаться.

UPDATE 2. Предложили еще несколько вариантов — MongoDB, elasticsearch, solr, sphinx, OrientDB. Планирую протестировать MongoDB, результаты тестов выложу тут же.

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 45

    +3
    в рамках решения моей задачи — использование Neo4j никаких плюсов не дало

    Потому-что ваши данные реляцыонные и отнюдь не графовые. Представление характеристик товаров графом не совсем корректно, и обогнать MySQL в таких задачах достаточно трудно.

    Намного интереснее было бы увидеть как раз построение запросов типа:
    «Выбрать контакты пользователей, которые лайкнули киноактерам, которые снялись в фильмах, в которых звучали саунтдтреки, которые были написаны музыкантами, которым я поставил лайк»


      0
      Потому-что ваши данные реляцыонные и отнюдь не графовые. Представление характеристик товаров графом не совсем корректно, и обогнать MySQL в таких задачах достаточно трудно.

      Я представлял в виде графа не характеристики товаров, а соответствие критериям фильтрации. Критерии далеко не всегда тривиальные, иногда 1 чекбокс в фильтре — это проверка значений у 5 параметров товара. Там JOIN'ится очень много всего — финальные SQL запросы получаются очень громоздкими.
      Финальное решение, конечно, очевидное — создать простую связь между критерием и товаром (подходит\не подходит), но мне оно пришло в голову только после использования Neo4j.
      В рамках текущего проекта у меня не получилось придумать применение для Neo4j — поэтому оформил в виде конспекта для себя «на потом».

      Намного интереснее было бы увидеть как раз построение запросов типа:
      «Выбрать контакты пользователей, которые лайкнули киноактерам, которые снялись в фильмах, в которых звучали саунтдтреки, которые были написаны музыкантами, которым я поставил лайк»

      По идее, примерно так
      MATCH (me:User {userId:123})-[:Like]->(musicants:User)-[:Author]->(s:Soundtrack)-[:Used]->(f:Film)<-[:Starred]-(actor: User)<-[:Like]-(u:User) RETURN u
      
      +4
      Задача поиска товара по группе фильтров — класическая задача для search engine а не database engine. Советую посмотреть в сторону elasticsearch:
      www.elasticsearch.org/

      Вот в частности решение одного из описанных кейсов для магазина: www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets.html

      Интеграция elasticsearch в Symfony + Doctrine — github.com/FriendsOfSymfony/FOSElasticaBundle
        0
        Просмотрел ссылку на документацию по ElasticSearch facets. Насколько я понял, вы предлагаете использовать facet'ы для критериев? Примерно, как теги в примере.

        Тогда у меня вопрос. Можно ли с помощью Facet'ов сделать запрос на поиск наподобие — найти статьи, у которых (есть тег1 И (есть тег2 ИЛИ есть тег3) И (есть тег4 ИЛИ есть тег 5 ИЛИ есть тег 6))?
        0
        Поддерживаю, в своем проекте работу с каталогом решили с помощью Elastic Search. Скорость и легкость масштабирования просто радует. Есть конечно нюансы, но они перекрываются полосами. Отмечу что документация на сайте не очень хорошая поэтому сразу предлагаю читать книгу Mastering Elastic Search. Там есть все ответы на ваши вопросы.
        0
        А это разве не самый обычный фасетный поиск, который легко реализуется на основе практически любого поискового движка?
        К примеру, как говорили люди выше: elasticsearch
          +4
          Я для примерно тех же целей использую NOSQL базу, конкретнее монгу. Ей все равно на структуру записи, и в то же время, искать по фильтрам и критериям очень просто.
            0
            главный (для меня) вопрос — это скорость выборки при поиске в базе Mongo из, например, 100.000 товаров, у каждого по 500 атрибутов (строковых\перечисляемых\числовых). По случайной комбинации из 20 параметров перемешанных через AND/OR. Именно это и планирую протестировать :)
              0
              Отпишитесь о результатах потом. И не забудьте индексы проставить.
                0
                У меня на мускуле работает это легко скорость около 0.05 сек на запросах в 20 параметров каши ор и анд
                  0
                  Тоже работает подобное на mysql, правда скорость на порядок меньше и база не очень большая. 0.05 с у вас случайно не по кэшированным запросам? По ним и у меня все быстро работает. Но проблема в том, что на таком кол-во вариантов кэш бессмысленен — слишком много комбинаций параметров.
                  Как вы храните характеристики в базе? У меня по сути EAV, правда слегка измененный вариант. У меня есть общий справочник характеристик, который к товарам никак не привязан. Непосредственно к товарам привязаны значения характеристик, а по значениям уже сами характеристики. У меня 4 типа характеристик (numeric, boolean, select, multiselect), по каждому свой запрос приходится делать. Надо наверное в отдельном посте это расписать :)
                    0
                    habrahabr.ru/post/45935/ примерно как описывал так и делал, EAV расширен тем, что string, int, float аттрибуты расскиданы по разным таблицам, чтобы были разные индексы, всякие select и multiselect сводятся к int через справочники. Да работает быстро без кеша ибо по бинарному индексу все, учтите так же что, выборка кластеризована по группам товаров и несмотря на то, что в базе ~ 300 000 товаров и 6 000 000 аттрибутов в каждой отдельной категории товаров обычно менее 10000 товаров и менее 200 000 аттрибутов и это радикально облегчает выборку. Короче делаем все по прямому индексу, ставим конечно памяти на сервер 16 гб и SSD диск, но на этом тюнинг кончается =)

                    Вообще все эти NoSQL для реляционных данных от безграмотности.
                      0
                      Select/Multiselect естественно тоже через справочники. А вот атрибуты по типам не раскидывал. А что это даст? Раз по каждому типу все равно идет отдельный запрос, то лучше вынести их в отдельные таблицы, чтобы поиск шел по меньшему кол-ву записей?

                      Ну и еще у меня обычный VDS за 1000р. в месяц, может в этом дело? :)
                        0
                        да, во все серьезные проекты я беру нормальные сервера.
                        0
                        UPD.
                        Изначально я задал разделение по типам потому что полагал, что хранить число в строке и вести по ним поиск — кощунственно, потому создаем 3 таблицы.
                        Глянул вашу статью. Ну так у меня поля все числовые, я строки не храню. Есть столбец value для boolean и numeric, есть столбец variant_id для select/multiselect (multiselect использует еще и value=0/1). Строковые характеристики отсутствуют как класс, нефиг — для порядку )) Правда value у меня float, наверное нехорошо…
                          0
                          не… MySQL в одном join с одной таблицы может использовать только один индекс, то есть если у вас в таблице аттрибутов 2 столбца: variant_id и value то индекс будет использоваться только по одному из них даже если он мультиколоночный по понятным причинам (там же нет подчинения связи variant_id и value) поэтому лучше делать больше таблиц, джойн на каждую будет использовать ее личный индекс — это позволяет искать все по индексам без переборов вообще
                          0
                          Кстати, а как вы решили вопрос с числовыми характеристиками, по которым мало вариантов? Ну например, слоты расширения в материнской плате. Их удобно задавать и хранить просто как число, но если вариантов мало, то в форме выводить не слайдер, а чекбоксы. Я пока налету конвертирую число в список значений, получая из базы все уникальные варианты. Похоже это тоже сильно сказывается на производительности, зато можно одной галочкой в настройках переключать вариант отображения по каждой характеристике.
                            0
                            Делаем ровно так же на производительности не сказывается вообще. просто проверьте что ваш запрос на уникальные использует индекс, должно летать.
                      0
                      Я вот все присматриваюсь к MongoDb для решения такой же задачи, и мучает меня вопрос. Из-за чего там достигается такая скорость поиска? Ведь принципиально новых способов повышения производительности в программировании никто не придумал — есть только два способа — платить либо ростом вычислительной мощности (распараллеливание), либо ростом хранилища (кэш). Правильно ли я понимаю, что по сути nosql по сравнения с sql — это своего рода кэш за счет денормализации, т.е. вместо лишних джоинов у нас все связанные данные уже есть в документе? Отсюда напрашивается другой вопрос — а как быть с изменениями данных? Если база денормализована, то при изменении связанных данных я должен пройтись по всем документам, где эти данные задействованы?
                        0
                        По сути да. Но это изначально неверный подход, ибо вся суть нерелятивных баз даных как раз в отсутствии отношений. Поэтому при использовании искусственных связей, та же монга начнет очень сильно отставать.
                          0
                          Не очень понял про искусственные связи. Если мне нужен именно справочник характеристик, чтобы в них был порядок, и каждый контент-менеджер не наполнял каталог, как ему вздумается, то связи там довольно очевидны. Само понятия справочника по своей природе «реляционно». Или вы хотите сказать, что не зачем использовать nosql там, где ему не место? В частности для подобного каталога.
                    0
                    Как раз для такой ситуации, когда структура товара не однородна и по ним требуется фильтр, подходят NoSQL решения. Притом на той же MongoDB ваша выборка
                    (характеристика1 = true AND (характеристика2 < 100)) OR (характеристика1 = false AND (характеристика3 > 17)) ... далее обычно мешанина из AND\OR
                    делается одной строчкой, со всеми сортировками, поисками по ключу, без каких-либо проблем.
                      +1
                      Смотрите вот вы отфильтровали товары. У вас есть еще 50 характеристик по которым можно фильтровать эти отфильтрованные товары. Как вы рассчитаете количество товаров для каждой из этих 50 характеристик в уже отфильтрованных товарах? 50 запросов дополнительных пошлете?

                      Как сказал товарищ выше это задача search engine, а не database engine.
                        0
                        В монге можно использовать aggregation framework для этого. Скажем, это не самое изящное решение, но, с другой стороны, в ряде случаев оно лучше, чем таскание за собой elasticsearch.

                        Более того, в некоторых ситуациях, aggregation framework даст больше возможностей.
                          0
                          в ряде случаев оно лучше, чем таскание за собой elasticsearch

                          Это если в проекте уже используется Mongo. Если нет — добавить что mongo, что elasticsearch одинаково
                            0
                            Я изучал возможность добавления elasticsearch, и если монгу можно использовать для решения различных задач (пускай даже иногда в компромиссном варианте), то elasticsearch в очень многих ситуациях избыточен, особенно для проектов среднего масштаба. Тем более учитывая, что это не универсальный инструмент.
                              +1
                              если монгу можно использовать для решения различных задач (пускай даже иногда в компромиссном варианте)
                              Иногда нужно делать одну задачу хорошо, а не несколько — посредственно
                              elasticsearch в очень многих ситуациях избыточен
                              я собственно с этим не спорю.

                              Обращаю внимание, что я не говорю, что elasticsearch лучше mongo, просто уточнил, что если в проекте используется только mysql, то добавление elasticsearch ничем не отличается от добавления mongo
                          0
                          В монго эт можно сделать одним запросом. Для таких случаев есть Aggregation framework

                          Если в двух словах объяснять: сначала $match для нужных нам фильтров, $unwind по оставшимся фильтрам, ну а далее стандартный $group на оставшиеся товары.

                          Как это работает можете посмотреть на примере в этой статье. Ничего против Seach engine не имею, но такие задачи можно решать и через NoSQL
                            +1
                            Давайте говорить не в общем NoSQL, а конкретно о MongoDB. Понятие NoSQL все-таки будет пошире. Я очень рад, что в MongoDB есть такой функционал. И мне даже стало интересно, что быстрее будет считать фасеты, MongoDB или Elasticsearch?
                              0
                              Elasticsearch быстрее, aggregation framework — это не самая быстрая часть экосистемы MongoDB. Хотя, опять же, все будет зависеть от размеров, думаю на сравнительно небольших объемах данных разница будет несущественна.
                                0
                                Не плохая тема для статьи на хабре )
                                  0
                                  У меня вопрос — если взять структуру данных как в примере сможет ли ElasticSearch сделать примерно такую выборку — найти статьи, у которых (есть тег1 И (есть тег2 ИЛИ есть тег3) И (есть тег4 ИЛИ есть тег 5 ИЛИ есть тег 6))?
                                    0
                                    Да Elastic легко справится с этой задачей.
                            0
                            задача решается элементарно с помощью solr или sphinx
                              0
                              Не сложно будет статейку написать, раз элементарно? Без иронии, вопрос удобной фильтрации товаров очень многим магазинам нужен.
                                0
                                Это нужно писать цикл, сначала как искать, это тоже многим не известно. Затем как искать с использованием facets. На цикл боюсь меня не хватит. Но если вы хоть раз использовали solr, то нужно просто указать facet fields и затем оперировать filters query для уточнения.

                                Вообщем там все легко, нужно просто один раз сделать :) На статью меня навряд ли хватит.
                                  +1
                                  Если есть возможность — сбросьте мне, пожалуйста, краткие инструкции в виде
                                  Меняем в настройках по умолчанию в solr вот это вот…
                                  Структура базы вот такая вот…
                                  Заполняем тестовую базу вот так вот…
                                  Для выборки по 3 критериям (кр1 AND (кр2 OR кр3)) пишим вот такой вот запрос…

                                  Я проведу тесты и добавлю в статью. Будет интересно добавить Mongo и Solr и сравнить в реальных условиях скорость их работы.
                                    0
                                    А как в solr вы реализуете вложенную группировку fq с использованием булевых операторов? Или придется в случаях сложной логики фильтрации все в main query писать?
                                    З.Ы. «Как искать» и «как искать с фасетами» знаю. Хотелось бы услышать ваше мнение/решение.
                                  0
                                  0
                                  Спасибо за статью… а то подали заявку на доклад

                                  Скажите, граф… Или об использовании NeoJ4 в веб-проекте
                                  devconf.ru/offers/10
                                  теперь понятно что за зверь такой :-)
                                    0
                                    и вам спасибо на добром слове.

                                    Neo4j забавный зверек. Забавы ради, его вполне можно использовать вместо традиционной СУБД, но реальная польза от этого будет далеко не во всех проектах.
                                    0
                                    Все тут принялись советовать всякие поисковые движки, которые являются не более чем костылями сверху неполноценных субд, не способных делать хитрые фильтрации за приемлемое время и без монструозных запросов. В итоге у вас получится не одна субд с шардингом, репликацией, транзакциями и прочими плюшками, а две с дополнительными проблемами синхронизации и прочим геморроем.

                                    Я бы рекомендовал посмотреть на OrientDB db-engines.com/en/system/Neo4j%3BOrientDB
                                      0
                                      Если есть возможность — сбросьте мне, пожалуйста, краткие инструкции в виде
                                      Меняем в настройках по умолчанию в OrientDB вот это вот…
                                      Структура базы вот такая вот…
                                      Заполняем тестовую базу вот так вот…
                                      Для выборки по 3 критериям (кр1 AND (кр2 OR кр3)) пишим вот такой вот запрос…

                                      Я проведу тесты и добавлю в статью. Будет интересно добавить еще Mongo, Solr, OrientDB и сравнить в реальных условиях скорость их работы.
                                      0
                                      Я решал как то так http://habrahabr.ru/post/114113/

                                      Only users with full accounts can post comments. Log in, please.