ensureSearchableColumns(); $value = strtolower($value); return $query->where(function($query) use ($columns, $value) { foreach ($columns as $column) { $query->orWhereRaw( 'LOWER(' . $column . ') LIKE ?', ['%' . $value . '%'] ); } }); } /** * Search the searchable columns phonetically (SOUNDEX / "sounds like"). * * @param \FluentCommunity\Framework\Database\Orm\Builder $query * @param string $value * @return \FluentCommunity\Framework\Database\Orm\Builder * @throws \Exception */ public function scopeSoundsLike($query, $value) { $columns = $this->ensureSearchableColumns(); return $query->where(function($query) use ($columns, $value) { foreach ($columns as $column) { $query->orWhereSoundsLike($column, $value); } }); } /** * Search the searchable columns by fuzzy edit-distance (Levenshtein). * * @param \FluentCommunity\Framework\Database\Orm\Builder $query * @param string $value * @param int $distance * @return \FluentCommunity\Framework\Database\Orm\Builder * @throws \Exception */ public function scopeSimilar($query, $value, $distance = 2) { $columns = $this->ensureSearchableColumns(); return $query->where(function($query) use ($columns, $value, $distance) { foreach ($columns as $column) { $query->orWhereSimilar($column, $value, $distance); } }); } /** * Search the searchable columns by full-text relevance, returning rows * ranked by how well they match (a "relevance" column is added). * * On MySQL/MariaDB this uses native FULLTEXT ranking and requires a * FULLTEXT index on the searchable columns (see Schema::fullText()). When * $fuzzy is true, recall is widened with a phonetic ("sounds like") match * so misspellings still surface (below the ranked hits). * * SQLite has no native FULLTEXT, so it degrades to a case-insensitive * LIKE match across the columns (unranked). * * @param \FluentCommunity\Framework\Database\Orm\Builder $query * @param string $value * @param bool $fuzzy * @return \FluentCommunity\Framework\Database\Orm\Builder * @throws \Exception */ public function scopeRelevanceSearch($query, $value, $fuzzy = false) { $columns = $this->ensureSearchableColumns(); if (Schema::isSqlite()) { return $query->where(function($query) use ($columns, $value, $fuzzy) { foreach ($columns as $column) { $query->orWhereLike($column, $value); if ($fuzzy) { $query->orWhereSoundsLike($column, $value); } } }); } return $query ->selectRelevance($columns, $value) ->where(function($query) use ($columns, $value, $fuzzy) { $query->whereFullText($columns, $value); if ($fuzzy) { foreach ($columns as $column) { $query->orWhereSoundsLike($column, $value); } } }) ->orderByRelevance(); } /** * Get the model's searchable columns, defaulting to an empty array when * the model does not declare the $searchableColumns property. * * @return array */ protected function getSearchableColumns() { return $this->searchableColumns ?? []; } /** * Ensure searchable columns are defined on the model and return them. * * @return array * @throws \Exception */ protected function ensureSearchableColumns() { $columns = $this->getSearchableColumns(); if (empty($columns)) { throw new Exception( 'No searchable columns were defined in ' . get_class($this) . '.' ); } return $columns; } }