1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
26:
27:
28: 29: 30: 31: 32: 33: 34:
35: class Mage_CatalogSearch_Model_Resource_Fulltext extends Mage_Core_Model_Resource_Db_Abstract
36: {
37: 38: 39: 40: 41:
42: protected $_searchableAttributes = null;
43:
44: 45: 46: 47: 48:
49: protected $_separator = '|';
50:
51: 52: 53: 54: 55:
56: protected $_dates = array();
57:
58: 59: 60: 61: 62:
63: protected $_productTypes = array();
64:
65: 66: 67: 68: 69:
70: protected $_engine = null;
71:
72: 73: 74: 75: 76: 77:
78: protected $_allowTableChanges = true;
79:
80:
81:
82:
83:
84: 85: 86: 87:
88: protected function _construct()
89: {
90: $this->_init('catalogsearch/fulltext', 'product_id');
91: $this->_engine = Mage::helper('catalogsearch')->getEngine();
92: }
93:
94: 95: 96: 97: 98:
99: public function getSeparator()
100: {
101: return $this->_separator;
102: }
103:
104: 105: 106: 107: 108: 109: 110:
111: public function rebuildIndex($storeId = null, $productIds = null)
112: {
113: if (is_null($storeId)) {
114: $storeIds = array_keys(Mage::app()->getStores());
115: foreach ($storeIds as $storeId) {
116: $this->_rebuildStoreIndex($storeId, $productIds);
117: }
118: } else {
119: $this->_rebuildStoreIndex($storeId, $productIds);
120: }
121:
122: return $this;
123: }
124:
125: 126: 127: 128: 129: 130: 131:
132: protected function _rebuildStoreIndex($storeId, $productIds = null)
133: {
134: $this->cleanIndex($storeId, $productIds);
135:
136:
137: $staticFields = array();
138: foreach ($this->_getSearchableAttributes('static') as $attribute) {
139: $staticFields[] = $attribute->getAttributeCode();
140: }
141: $dynamicFields = array(
142: 'int' => array_keys($this->_getSearchableAttributes('int')),
143: 'varchar' => array_keys($this->_getSearchableAttributes('varchar')),
144: 'text' => array_keys($this->_getSearchableAttributes('text')),
145: 'decimal' => array_keys($this->_getSearchableAttributes('decimal')),
146: 'datetime' => array_keys($this->_getSearchableAttributes('datetime')),
147: );
148:
149:
150: $visibility = $this->_getSearchableAttribute('visibility');
151: $status = $this->_getSearchableAttribute('status');
152: $statusVals = Mage::getSingleton('catalog/product_status')->getVisibleStatusIds();
153: $allowedVisibilityValues = $this->_engine->getAllowedVisibility();
154:
155: $lastProductId = 0;
156: while (true) {
157: $products = $this->_getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId);
158: if (!$products) {
159: break;
160: }
161:
162: $productAttributes = array();
163: $productRelations = array();
164: foreach ($products as $productData) {
165: $lastProductId = $productData['entity_id'];
166: $productAttributes[$productData['entity_id']] = $productData['entity_id'];
167: $productChildren = $this->_getProductChildIds($productData['entity_id'], $productData['type_id']);
168: $productRelations[$productData['entity_id']] = $productChildren;
169: if ($productChildren) {
170: foreach ($productChildren as $productChildId) {
171: $productAttributes[$productChildId] = $productChildId;
172: }
173: }
174: }
175:
176: $productIndexes = array();
177: $productAttributes = $this->_getProductAttributes($storeId, $productAttributes, $dynamicFields);
178: foreach ($products as $productData) {
179: if (!isset($productAttributes[$productData['entity_id']])) {
180: continue;
181: }
182:
183: $productAttr = $productAttributes[$productData['entity_id']];
184: if (!isset($productAttr[$visibility->getId()])
185: || !in_array($productAttr[$visibility->getId()], $allowedVisibilityValues)
186: ) {
187: continue;
188: }
189: if (!isset($productAttr[$status->getId()]) || !in_array($productAttr[$status->getId()], $statusVals)) {
190: continue;
191: }
192:
193: $productIndex = array(
194: $productData['entity_id'] => $productAttr
195: );
196:
197: if ($productChildren = $productRelations[$productData['entity_id']]) {
198: foreach ($productChildren as $productChildId) {
199: if (isset($productAttributes[$productChildId])) {
200: $productIndex[$productChildId] = $productAttributes[$productChildId];
201: }
202: }
203: }
204:
205: $index = $this->_prepareProductIndex($productIndex, $productData, $storeId);
206:
207: $productIndexes[$productData['entity_id']] = $index;
208: }
209:
210: $this->_saveProductIndexes($storeId, $productIndexes);
211: }
212:
213: $this->resetSearchResults();
214:
215: return $this;
216: }
217:
218: 219: 220: 221: 222: 223: 224: 225: 226: 227:
228: protected function _getSearchableProducts($storeId, array $staticFields, $productIds = null, $lastProductId = 0,
229: $limit = 100)
230: {
231: $websiteId = Mage::app()->getStore($storeId)->getWebsiteId();
232: $writeAdapter = $this->_getWriteAdapter();
233:
234: $select = $writeAdapter->select()
235: ->useStraightJoin(true)
236: ->from(
237: array('e' => $this->getTable('catalog/product')),
238: array_merge(array('entity_id', 'type_id'), $staticFields)
239: )
240: ->join(
241: array('website' => $this->getTable('catalog/product_website')),
242: $writeAdapter->quoteInto(
243: 'website.product_id=e.entity_id AND website.website_id=?',
244: $websiteId
245: ),
246: array()
247: )
248: ->join(
249: array('stock_status' => $this->getTable('cataloginventory/stock_status')),
250: $writeAdapter->quoteInto(
251: 'stock_status.product_id=e.entity_id AND stock_status.website_id=?',
252: $websiteId
253: ),
254: array('in_stock' => 'stock_status')
255: );
256:
257: if (!is_null($productIds)) {
258: $select->where('e.entity_id IN(?)', $productIds);
259: }
260:
261: $select->where('e.entity_id>?', $lastProductId)
262: ->limit($limit)
263: ->order('e.entity_id');
264:
265: $result = $writeAdapter->fetchAll($select);
266:
267: return $result;
268: }
269:
270: 271: 272: 273: 274:
275: public function resetSearchResults()
276: {
277: $adapter = $this->_getWriteAdapter();
278: $adapter->update($this->getTable('catalogsearch/search_query'), array('is_processed' => 0));
279: $adapter->delete($this->getTable('catalogsearch/result'));
280:
281: Mage::dispatchEvent('catalogsearch_reset_search_result');
282:
283: return $this;
284: }
285:
286: 287: 288: 289: 290: 291: 292:
293: public function cleanIndex($storeId = null, $productId = null)
294: {
295: if ($this->_engine) {
296: $this->_engine->cleanIndex($storeId, $productId);
297: }
298:
299: return $this;
300: }
301:
302: 303: 304: 305: 306: 307: 308: 309:
310: public function prepareResult($object, $queryText, $query)
311: {
312: $adapter = $this->_getWriteAdapter();
313: if (!$query->getIsProcessed()) {
314: $searchType = $object->getSearchType($query->getStoreId());
315:
316: $preparedTerms = Mage::getResourceHelper('catalogsearch')
317: ->prepareTerms($queryText, $query->getMaxQueryWords());
318:
319: $bind = array();
320: $like = array();
321: $likeCond = '';
322: if ($searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_LIKE
323: || $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_COMBINE
324: ) {
325: $helper = Mage::getResourceHelper('core');
326: $words = Mage::helper('core/string')->splitWords($queryText, true, $query->getMaxQueryWords());
327: foreach ($words as $word) {
328: $like[] = $helper->getCILike('s.data_index', $word, array('position' => 'any'));
329: }
330: if ($like) {
331: $likeCond = '(' . join(' OR ', $like) . ')';
332: }
333: }
334: $mainTableAlias = 's';
335: $fields = array(
336: 'query_id' => new Zend_Db_Expr($query->getId()),
337: 'product_id',
338: );
339: $select = $adapter->select()
340: ->from(array($mainTableAlias => $this->getMainTable()), $fields)
341: ->joinInner(array('e' => $this->getTable('catalog/product')),
342: 'e.entity_id = s.product_id',
343: array())
344: ->where($mainTableAlias.'.store_id = ?', (int)$query->getStoreId());
345:
346: if ($searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_FULLTEXT
347: || $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_COMBINE
348: ) {
349: $bind[':query'] = implode(' ', $preparedTerms[0]);
350: $where = Mage::getResourceHelper('catalogsearch')
351: ->chooseFulltext($this->getMainTable(), $mainTableAlias, $select);
352: }
353:
354: if ($likeCond != '' && $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_COMBINE) {
355: $where .= ($where ? ' OR ' : '') . $likeCond;
356: } elseif ($likeCond != '' && $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_LIKE) {
357: $select->columns(array('relevance' => new Zend_Db_Expr(0)));
358: $where = $likeCond;
359: }
360:
361: if ($where != '') {
362: $select->where($where);
363: }
364:
365: $sql = $adapter->insertFromSelect($select,
366: $this->getTable('catalogsearch/result'),
367: array(),
368: Varien_Db_Adapter_Interface::INSERT_ON_DUPLICATE);
369: $adapter->query($sql, $bind);
370:
371: $query->setIsProcessed(1);
372: }
373:
374: return $this;
375: }
376:
377: 378: 379: 380: 381:
382: public function getEavConfig()
383: {
384: return Mage::getSingleton('eav/config');
385: }
386:
387: 388: 389: 390: 391: 392:
393: protected function _getSearchableAttributes($backendType = null)
394: {
395: if (is_null($this->_searchableAttributes)) {
396: $this->_searchableAttributes = array();
397:
398: $productAttributeCollection = Mage::getResourceModel('catalog/product_attribute_collection');
399:
400: if ($this->_engine && $this->_engine->allowAdvancedIndex()) {
401: $productAttributeCollection->addToIndexFilter(true);
402: } else {
403: $productAttributeCollection->addSearchableAttributeFilter();
404: }
405: $attributes = $productAttributeCollection->getItems();
406:
407: Mage::dispatchEvent('catelogsearch_searchable_attributes_load_after', array(
408: 'engine' => $this->_engine,
409: 'attributes' => $attributes
410: ));
411:
412: $entity = $this->getEavConfig()
413: ->getEntityType(Mage_Catalog_Model_Product::ENTITY)
414: ->getEntity();
415:
416: foreach ($attributes as $attribute) {
417: $attribute->setEntity($entity);
418: }
419:
420: $this->_searchableAttributes = $attributes;
421: }
422:
423: if (!is_null($backendType)) {
424: $attributes = array();
425: foreach ($this->_searchableAttributes as $attributeId => $attribute) {
426: if ($attribute->getBackendType() == $backendType) {
427: $attributes[$attributeId] = $attribute;
428: }
429: }
430:
431: return $attributes;
432: }
433:
434: return $this->_searchableAttributes;
435: }
436:
437: 438: 439: 440: 441: 442:
443: protected function _getSearchableAttribute($attribute)
444: {
445: $attributes = $this->_getSearchableAttributes();
446: if (is_numeric($attribute)) {
447: if (isset($attributes[$attribute])) {
448: return $attributes[$attribute];
449: }
450: } elseif (is_string($attribute)) {
451: foreach ($attributes as $attributeModel) {
452: if ($attributeModel->getAttributeCode() == $attribute) {
453: return $attributeModel;
454: }
455: }
456: }
457:
458: return $this->getEavConfig()->getAttribute(Mage_Catalog_Model_Product::ENTITY, $attribute);
459: }
460:
461: 462: 463: 464: 465: 466: 467:
468: protected function _unifyField($field, $backendType = 'varchar')
469: {
470: if ($backendType == 'datetime') {
471: $expr = Mage::getResourceHelper('catalogsearch')->castField(
472: $this->_getReadAdapter()->getDateFormatSql($field, '%Y-%m-%d %H:%i:%s'));
473: } else {
474: $expr = Mage::getResourceHelper('catalogsearch')->castField($field);
475: }
476: return $expr;
477: }
478:
479: 480: 481: 482: 483: 484: 485: 486:
487: protected function _getProductAttributes($storeId, array $productIds, array $attributeTypes)
488: {
489: $result = array();
490: $selects = array();
491: $adapter = $this->_getWriteAdapter();
492: $ifStoreValue = $adapter->getCheckSql('t_store.value_id > 0', 't_store.value', 't_default.value');
493: foreach ($attributeTypes as $backendType => $attributeIds) {
494: if ($attributeIds) {
495: $tableName = $this->getTable(array('catalog/product', $backendType));
496: $selects[] = $adapter->select()
497: ->from(
498: array('t_default' => $tableName),
499: array('entity_id', 'attribute_id'))
500: ->joinLeft(
501: array('t_store' => $tableName),
502: $adapter->quoteInto(
503: 't_default.entity_id=t_store.entity_id' .
504: ' AND t_default.attribute_id=t_store.attribute_id' .
505: ' AND t_store.store_id=?',
506: $storeId),
507: array('value' => $this->_unifyField($ifStoreValue, $backendType)))
508: ->where('t_default.store_id=?', 0)
509: ->where('t_default.attribute_id IN (?)', $attributeIds)
510: ->where('t_default.entity_id IN (?)', $productIds);
511: }
512: }
513:
514: if ($selects) {
515: $select = $adapter->select()->union($selects, Zend_Db_Select::SQL_UNION_ALL);
516: $query = $adapter->query($select);
517: while ($row = $query->fetch()) {
518: $result[$row['entity_id']][$row['attribute_id']] = $row['value'];
519: }
520: }
521:
522: return $result;
523: }
524:
525: 526: 527: 528: 529: 530:
531: protected function _getProductTypeInstance($typeId)
532: {
533: if (!isset($this->_productTypes[$typeId])) {
534: $productEmulator = $this->_getProductEmulator();
535: $productEmulator->setTypeId($typeId);
536:
537: $this->_productTypes[$typeId] = Mage::getSingleton('catalog/product_type')
538: ->factory($productEmulator);
539: }
540: return $this->_productTypes[$typeId];
541: }
542:
543: 544: 545: 546: 547: 548: 549:
550: protected function _getProductChildIds($productId, $typeId)
551: {
552: $typeInstance = $this->_getProductTypeInstance($typeId);
553: $relation = $typeInstance->isComposite()
554: ? $typeInstance->getRelationInfo()
555: : false;
556:
557: if ($relation && $relation->getTable() && $relation->getParentFieldName() && $relation->getChildFieldName()) {
558: $select = $this->_getReadAdapter()->select()
559: ->from(
560: array('main' => $this->getTable($relation->getTable())),
561: array($relation->getChildFieldName()))
562: ->where("{$relation->getParentFieldName()}=?", $productId);
563: if (!is_null($relation->getWhere())) {
564: $select->where($relation->getWhere());
565: }
566: return $this->_getReadAdapter()->fetchCol($select);
567: }
568:
569: return null;
570: }
571:
572: 573: 574: 575: 576:
577: protected function _getProductEmulator()
578: {
579: $productEmulator = new Varien_Object();
580: $productEmulator->setIdFieldName('entity_id');
581:
582: return $productEmulator;
583: }
584:
585: 586: 587: 588: 589: 590: 591: 592:
593: protected function _prepareProductIndex($indexData, $productData, $storeId)
594: {
595: $index = array();
596:
597: foreach ($this->_getSearchableAttributes('static') as $attribute) {
598: $attributeCode = $attribute->getAttributeCode();
599:
600: if (isset($productData[$attributeCode])) {
601: $value = $this->_getAttributeValue($attribute->getId(), $productData[$attributeCode], $storeId);
602: if ($value) {
603:
604: if (isset($index[$attributeCode])) {
605: if (!is_array($index[$attributeCode])) {
606: $index[$attributeCode] = array($index[$attributeCode]);
607: }
608: $index[$attributeCode][] = $value;
609: }
610:
611: else {
612: $index[$attributeCode] = $value;
613: }
614: }
615: }
616: }
617:
618: foreach ($indexData as $entityId => $attributeData) {
619: foreach ($attributeData as $attributeId => $attributeValue) {
620: $value = $this->_getAttributeValue($attributeId, $attributeValue, $storeId);
621: if (!is_null($value) && $value !== false) {
622: $attributeCode = $this->_getSearchableAttribute($attributeId)->getAttributeCode();
623:
624: if (isset($index[$attributeCode])) {
625: $index[$attributeCode][$entityId] = $value;
626: } else {
627: $index[$attributeCode] = array($entityId => $value);
628: }
629: }
630: }
631: }
632:
633: if (!$this->_engine->allowAdvancedIndex()) {
634: $product = $this->_getProductEmulator()
635: ->setId($productData['entity_id'])
636: ->setTypeId($productData['type_id'])
637: ->setStoreId($storeId);
638: $typeInstance = $this->_getProductTypeInstance($productData['type_id']);
639: if ($data = $typeInstance->getSearchableData($product)) {
640: $index['options'] = $data;
641: }
642: }
643:
644: if (isset($productData['in_stock'])) {
645: $index['in_stock'] = $productData['in_stock'];
646: }
647:
648: if ($this->_engine) {
649: return $this->_engine->prepareEntityIndex($index, $this->_separator);
650: }
651:
652: return Mage::helper('catalogsearch')->prepareIndexdata($index, $this->_separator);
653: }
654:
655: 656: 657: 658: 659: 660: 661: 662:
663: protected function _getAttributeValue($attributeId, $value, $storeId)
664: {
665: $attribute = $this->_getSearchableAttribute($attributeId);
666: if (!$attribute->getIsSearchable()) {
667: if ($this->_engine->allowAdvancedIndex()) {
668: if ($attribute->getAttributeCode() == 'visibility') {
669: return $value;
670: } elseif (!($attribute->getIsVisibleInAdvancedSearch()
671: || $attribute->getIsFilterable()
672: || $attribute->getIsFilterableInSearch()
673: || $attribute->getUsedForSortBy())
674: ) {
675: return null;
676: }
677: } else {
678: return null;
679: }
680: }
681:
682: if ($attribute->usesSource()) {
683: if ($this->_engine->allowAdvancedIndex()) {
684: return $value;
685: }
686:
687: $attribute->setStoreId($storeId);
688: $value = $attribute->getSource()->getOptionText($value);
689:
690: if (is_array($value)) {
691: $value = implode($this->_separator, $value);
692: } elseif (empty($value)) {
693: $inputType = $attribute->getFrontend()->getInputType();
694: if ($inputType == 'select' || $inputType == 'multiselect') {
695: return null;
696: }
697: }
698: } elseif ($attribute->getBackendType() == 'datetime') {
699: $value = $this->_getStoreDate($storeId, $value);
700: } else {
701: $inputType = $attribute->getFrontend()->getInputType();
702: if ($inputType == 'price') {
703: $value = Mage::app()->getStore($storeId)->roundPrice($value);
704: }
705: }
706:
707: $value = preg_replace("#\s+#siu", ' ', trim(strip_tags($value)));
708:
709: return $value;
710: }
711:
712: 713: 714: 715: 716: 717: 718: 719:
720: protected function _saveProductIndex($productId, $storeId, $index)
721: {
722: if ($this->_engine) {
723: $this->_engine->saveEntityIndex($productId, $storeId, $index);
724: }
725:
726: return $this;
727: }
728:
729: 730: 731: 732: 733: 734: 735:
736: protected function _saveProductIndexes($storeId, $productIndexes)
737: {
738: if ($this->_engine) {
739: $this->_engine->saveEntityIndexes($storeId, $productIndexes);
740: }
741:
742: return $this;
743: }
744:
745: 746: 747: 748: 749: 750: 751:
752: protected function _getStoreDate($storeId, $date = null)
753: {
754: if (!isset($this->_dates[$storeId])) {
755: $timezone = Mage::getStoreConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE, $storeId);
756: $locale = Mage::getStoreConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_LOCALE, $storeId);
757: $locale = new Zend_Locale($locale);
758:
759: $dateObj = new Zend_Date(null, null, $locale);
760: $dateObj->setTimezone($timezone);
761: $this->_dates[$storeId] = array($dateObj, $locale->getTranslation(null, 'date', $locale));
762: }
763:
764: if (!is_empty_date($date)) {
765: list($dateObj, $format) = $this->_dates[$storeId];
766: $dateObj->setDate($date, Varien_Date::DATETIME_INTERNAL_FORMAT);
767:
768: return $dateObj->toString($format);
769: }
770:
771: return null;
772: }
773:
774:
775:
776:
777:
778:
779:
780: 781: 782: 783: 784: 785: 786:
787: public function setAllowTableChanges($value = true)
788: {
789: $this->_allowTableChanges = $value;
790: return $this;
791: }
792:
793: 794: 795: 796: 797: 798: 799: 800: 801:
802: public function updateCategoryIndex($productIds, $categoryIds)
803: {
804: return $this;
805: }
806: }
807: