Improve investor quick search with typed counts, product categories and licensing guidance
This commit is contained in:
@@ -7,6 +7,64 @@ use Illuminate\Support\Str;
|
||||
/** Text matching shared by the database and the built-in page catalogue. */
|
||||
final class SearchTerms
|
||||
{
|
||||
/** @return array{type:string,category:?string,label:string,kind:?string,filter:string}|null */
|
||||
public function requestIntent(string $query): ?array
|
||||
{
|
||||
if ($count = $this->countIntent($query)) {
|
||||
return ['type' => 'count', ...$count];
|
||||
}
|
||||
$normalized = trim(mb_strtolower(Str::ascii($query)), " \t\n\r\0\x0B?!.");
|
||||
if (preg_match('/^(?:como|onde|quero|preciso de)\s+(?:(?:posso|faco para|devo)\s+)?licenciar(?:$|\s+(?:(?:uma|a|minha|o|um|meu)\s+)?(?:empresa|negocio|actividade|atividade|investimento)\b)/', $normalized)) {
|
||||
return ['type' => 'guide', 'category' => 'Páginas e serviços', 'label' => 'Licenciamento da empresa', 'kind' => null, 'filter' => 'licenciamento'];
|
||||
}
|
||||
$product = null;
|
||||
if (preg_match('/^(?:(?:tipos?|lista|catalogo) de |quais (?:sao )?(?:os )?)?produtos?\b(.*)$/', $normalized, $match)) {
|
||||
$product = $match[1];
|
||||
} elseif (preg_match('/^onde (?:encontrar|produzir|se produz|existem|ha)\s+(.+)$/', $normalized, $match)) {
|
||||
$product = $match[1];
|
||||
}
|
||||
if ($product !== null) {
|
||||
$category = preg_match('/\bfinanceiros?\b/', $product) ? 'Produtos financeiros' : (preg_match('/\bfisicos?\b/', $product) ? 'Produtos e localizações' : null);
|
||||
$product = preg_replace('/\b(?:e (?:a )?(?:sua )?localizacao|e (?:as )?(?:suas )?localizacoes|financeiros?|fisicos?|existem|disponiveis|no portal)\b/', ' ', $product);
|
||||
|
||||
return ['type' => 'products', 'category' => $category, 'label' => 'Produtos e localização', 'kind' => null, 'filter' => trim(preg_replace('/\s+/', ' ', $product))];
|
||||
}
|
||||
if (preg_match('/^(?:listar|mostrar|mostre|ver|quais (?:sao )?(?:os|as))\s+(.+)$/', $normalized, $match)
|
||||
&& ($list = $this->countIntent('quantos '.$match[1]))) {
|
||||
return ['type' => 'list', ...$list];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return array{category:string,label:string,kind:?string,filter:string}|null */
|
||||
public function countIntent(string $query): ?array
|
||||
{
|
||||
$query = trim(mb_strtolower(Str::ascii($query)), " \t\n\r\0\x0B?!.");
|
||||
$types = [
|
||||
'corredor|corredores' => ['Carteira: projectos, clusters e corredores', 'corredores', 'corridor'],
|
||||
'cluster|clusters' => ['Carteira: projectos, clusters e corredores', 'clusters', 'cluster'],
|
||||
'projectos? ancora|projetos? ancora' => ['Carteira: projectos, clusters e corredores', 'projectos âncora', 'project'],
|
||||
'oportunidade|oportunidades' => ['Oportunidades', 'oportunidades', null],
|
||||
'negocio|negocios' => ['Negócios', 'negócios', null],
|
||||
'documento|documentos' => ['Documentos e legislação', 'documentos', null],
|
||||
'artigo|artigos' => ['Artigos', 'artigos', null],
|
||||
'sector|sectores|setor|setores' => ['Sectores', 'sectores', null],
|
||||
'localizacao|localizacoes' => ['Localizações', 'localizações', null],
|
||||
'zonas? de investimento|zona|zonas' => ['Zonas de investimento', 'zonas de investimento', null],
|
||||
'parceria|parcerias' => ['Parcerias', 'parcerias', null],
|
||||
];
|
||||
foreach ($types as $pattern => [$category, $label, $kind]) {
|
||||
if (preg_match('/^(?:quantos|quantas|numero de|quantidade de|total de|qual (?:e )?o numero de)\s+(?:'.$pattern.')\b(.*)$/u', $query, $match)) {
|
||||
$filter = preg_replace('/\b(?:no portal|no investemoz|em mocambique|de investimento|existem|existe|existentes|existente|ha|temos|estao|sao|disponiveis|disponivel|publicados|publicadas|registados|registadas|cadastrados|cadastradas)\b/u', ' ', $match[1]);
|
||||
|
||||
return ['category' => $category, 'label' => $label, 'kind' => $kind, 'filter' => trim(preg_replace('/\s+/', ' ', $filter))];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function tokens(string $query): array
|
||||
{
|
||||
@@ -24,13 +82,16 @@ final class SearchTerms
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/** Known Portuguese spelling and number variants; do not infer semantic equivalence. */
|
||||
/** Known Portuguese spelling and number variants; do not infer semantic equivalence.
|
||||
* @return list<string>
|
||||
*/
|
||||
public function variants(string $token): array
|
||||
{
|
||||
foreach ([['sector', 'sectores', 'setor', 'setores'], ['projecto', 'projectos', 'projeto', 'projetos'],
|
||||
['investir', 'investimento', 'investimentos'], ['investidor', 'investidores'], ['oportunidade', 'oportunidades'], ['documento', 'documentos'],
|
||||
['empresa', 'empresas'], ['parceria', 'parcerias'], ['zona', 'zonas'], ['formulario', 'formularios'],
|
||||
['provincia', 'provincias'], ['corredor', 'corredores'], ['cluster', 'clusters'], ['imposto', 'impostos'], ['licenca', 'licencas']] as $group) {
|
||||
['provincia', 'provincias'], ['corredor', 'corredores'], ['cluster', 'clusters'], ['imposto', 'impostos'], ['licenca', 'licencas'],
|
||||
['banco', 'bancos'], ['seguro', 'seguros'], ['produto', 'produtos'], ['agricola', 'agricolas']] as $group) {
|
||||
if (in_array($token, $group, true)) {
|
||||
return $group;
|
||||
}
|
||||
@@ -44,7 +105,10 @@ final class SearchTerms
|
||||
return '%'.str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $token).'%';
|
||||
}
|
||||
|
||||
/** Columns are trusted application constants, never user input. */
|
||||
/** Columns are trusted application constants, never user input.
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Database\Query\Builder $query
|
||||
* @param list<literal-string> $columns
|
||||
*/
|
||||
public function orFields($query, array $columns, string $token): void
|
||||
{
|
||||
foreach ($columns as $column) {
|
||||
@@ -54,6 +118,10 @@ final class SearchTerms
|
||||
}
|
||||
}
|
||||
|
||||
/** @param \Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Database\Query\Builder $query
|
||||
* @param list<literal-string> $columns
|
||||
* @param list<string> $tokens
|
||||
*/
|
||||
public function constrain($query, array $columns, array $tokens): void
|
||||
{
|
||||
foreach ($tokens as $token) {
|
||||
@@ -61,6 +129,7 @@ final class SearchTerms
|
||||
}
|
||||
}
|
||||
|
||||
/** @param list<string> $tokens */
|
||||
public function matches(string $text, array $tokens): bool
|
||||
{
|
||||
$text = mb_strtolower(Str::ascii($text));
|
||||
@@ -73,11 +142,15 @@ final class SearchTerms
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param literal-string $column
|
||||
* @return literal-string
|
||||
*/
|
||||
private function normalizedColumn(string $column): string
|
||||
{
|
||||
return "translate(lower(coalesce({$column}::text, '')), 'áàâãäéèêëíìîïóòôõöúùûüçñ', 'aaaaaeeeeiiiiooooouuuucn')";
|
||||
}
|
||||
|
||||
/** @param list<string> $tokens */
|
||||
public function score(string $title, string $phrase, array $tokens): int
|
||||
{
|
||||
$title = mb_strtolower(Str::ascii($title));
|
||||
@@ -92,7 +165,12 @@ final class SearchTerms
|
||||
return $score;
|
||||
}
|
||||
|
||||
/** The same title score used for in-memory pages, with bound search terms. */
|
||||
/** The same title score used for in-memory pages, with bound search terms.
|
||||
* @param \Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Database\Query\Builder $query
|
||||
* @param list<string> $tokens
|
||||
* @param literal-string $titleColumn
|
||||
* @param literal-string $tieColumn
|
||||
*/
|
||||
public function orderByRelevance($query, string $titleColumn, string $phrase, array $tokens, string $tieColumn): void
|
||||
{
|
||||
$column = $this->normalizedColumn($titleColumn);
|
||||
@@ -107,6 +185,12 @@ final class SearchTerms
|
||||
}
|
||||
$sql .= ' + CASE WHEN ('.implode(' OR ', $conditions).') THEN 1 ELSE 0 END';
|
||||
}
|
||||
$query->orderByRaw('('.$sql.') DESC', $bindings)->orderBy($titleColumn)->orderBy($tieColumn);
|
||||
$query->orderByRaw('('.$sql.') DESC', $bindings);
|
||||
if (str_contains($titleColumn, '(')) {
|
||||
$query->orderByRaw($titleColumn);
|
||||
} else {
|
||||
$query->orderBy($titleColumn);
|
||||
}
|
||||
$query->orderBy($tieColumn);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user