Search improvements.

This commit is contained in:
James Cole
2019-03-02 14:12:09 +01:00
parent af07522f16
commit 87d5cabe52
16 changed files with 226 additions and 37 deletions

View File

@@ -67,14 +67,10 @@ class SearchController extends Controller
// parse search terms: // parse search terms:
$searcher->parseQuery($fullQuery); $searcher->parseQuery($fullQuery);
$query = $searcher->getWordsAsString(); $query = $searcher->getWordsAsString();
$modifiers = $searcher->getModifiers();
//also get modifiers to display:
$subTitle = (string)trans('breadcrumbs.search_result', ['query' => $query]); $subTitle = (string)trans('breadcrumbs.search_result', ['query' => $query]);
return view('search.index', compact('query', 'fullQuery', 'subTitle')); return view('search.index', compact('query','modifiers', 'fullQuery', 'subTitle'));
} }
/** /**
@@ -92,9 +88,10 @@ class SearchController extends Controller
$searcher->parseQuery($fullQuery); $searcher->parseQuery($fullQuery);
$searcher->setLimit((int)config('firefly.search_result_limit')); $searcher->setLimit((int)config('firefly.search_result_limit'));
$transactions = $searcher->searchTransactions(); $transactions = $searcher->searchTransactions();
$searchTime = $searcher->searchTime(); // in seconds
try { try {
$html = view('search.search', compact('transactions'))->render(); $html = view('search.search', compact('transactions','searchTime'))->render();
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error(sprintf('Cannot render search.search: %s', $e->getMessage())); Log::error(sprintf('Cannot render search.search: %s', $e->getMessage()));

View File

@@ -556,6 +556,25 @@ class AccountRepository implements AccountRepositoryInterface
return $result; return $result;
} }
/**
* @param string $query
* @param array $types
*
* @return Collection
*/
public function searchAccount(string $query, array $types): Collection
{
$dbQuery = $this->user->accounts();
$search = sprintf('%%%s%%', $query);
if (\count($types) > 0) {
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
$dbQuery->whereIn('account_types.type', $types);
}
$dbQuery->where('name', 'LIKE', $search);
return $dbQuery->get(['accounts.*']);
}
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -36,6 +36,7 @@ use Illuminate\Support\Collection;
*/ */
interface AccountRepositoryInterface interface AccountRepositoryInterface
{ {
/** /**
* Moved here from account CRUD. * Moved here from account CRUD.
* *
@@ -247,6 +248,14 @@ interface AccountRepositoryInterface
*/ */
public function oldestJournalDate(Account $account): ?Carbon; public function oldestJournalDate(Account $account): ?Carbon;
/**
* @param string $query
* @param array $types
*
* @return Collection
*/
public function searchAccount(string $query, array $types): Collection;
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -623,6 +623,18 @@ class BillRepository implements BillRepositoryInterface
return $start; return $start;
} }
/**
* @param string $query
*
* @return Collection
*/
public function searchBill(string $query): Collection
{
$query = sprintf('%%%s%%', $query);
return $this->user->bills()->where('name', 'LIKE', $query)->get();
}
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -33,7 +33,6 @@ use Illuminate\Support\Collection;
*/ */
interface BillRepositoryInterface interface BillRepositoryInterface
{ {
/** /**
* @param Bill $bill * @param Bill $bill
* *
@@ -236,6 +235,13 @@ interface BillRepositoryInterface
*/ */
public function nextExpectedMatch(Bill $bill, Carbon $date): Carbon; public function nextExpectedMatch(Bill $bill, Carbon $date): Carbon;
/**
* @param string $query
*
* @return Collection
*/
public function searchBill(string $query): Collection;
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -635,6 +635,20 @@ class BudgetRepository implements BudgetRepositoryInterface
return $result; return $result;
} }
/**
* @param string $query
*
* @return Collection
*/
public function searchBudget(string $query): Collection
{
$query = sprintf('%%%s%%', $query);
return $this->user->budgets()->where('name', 'LIKE', $query)->get();
}
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param TransactionCurrency $currency * @param TransactionCurrency $currency
* @param Carbon $start * @param Carbon $start
@@ -662,8 +676,6 @@ class BudgetRepository implements BudgetRepositoryInterface
return $availableBudget; return $availableBudget;
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Budget $budget * @param Budget $budget
* @param int $order * @param int $order
@@ -903,6 +915,8 @@ class BudgetRepository implements BudgetRepositoryInterface
return $limit; return $limit;
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Budget $budget * @param Budget $budget
* @param array $data * @param array $data
@@ -922,8 +936,6 @@ class BudgetRepository implements BudgetRepositoryInterface
return $budget; return $budget;
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param AvailableBudget $availableBudget * @param AvailableBudget $availableBudget
* @param array $data * @param array $data

View File

@@ -169,8 +169,6 @@ interface BudgetRepositoryInterface
*/ */
public function getBudgets(): Collection; public function getBudgets(): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* Get all budgets with these ID's. * Get all budgets with these ID's.
* *
@@ -180,6 +178,8 @@ interface BudgetRepositoryInterface
*/ */
public function getByIds(array $budgetIds): Collection; public function getByIds(array $budgetIds): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @return Collection * @return Collection
*/ */
@@ -194,6 +194,13 @@ interface BudgetRepositoryInterface
*/ */
public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array; public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array;
/**
* @param string $query
*
* @return Collection
*/
public function searchBudget(string $query): Collection;
/** /**
* @param TransactionCurrency $currency * @param TransactionCurrency $currency
* @param Carbon $start * @param Carbon $start

View File

@@ -531,6 +531,18 @@ class CategoryRepository implements CategoryRepositoryInterface
/** @noinspection MoreThanThreeArgumentsInspection */ /** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param string $query
*
* @return Collection
*/
public function searchCategory(string $query): Collection
{
$query = sprintf('%%%s%%', $query);
return $this->user->categories()->where('name', 'LIKE', $query)->get();
}
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -40,7 +40,6 @@ interface CategoryRepositoryInterface
*/ */
public function destroy(Category $category): bool; public function destroy(Category $category): bool;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Collection $categories * @param Collection $categories
* @param Collection $accounts * @param Collection $accounts
@@ -52,6 +51,7 @@ interface CategoryRepositoryInterface
public function earnedInPeriod(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): string; public function earnedInPeriod(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): string;
/** @noinspection MoreThanThreeArgumentsInspection */ /** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Collection $categories * @param Collection $categories
* @param Collection $accounts * @param Collection $accounts
@@ -62,6 +62,8 @@ interface CategoryRepositoryInterface
*/ */
public function earnedInPeriodCollection(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): Collection; public function earnedInPeriodCollection(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* A very cryptic method name that means: * A very cryptic method name that means:
* *
@@ -119,8 +121,6 @@ interface CategoryRepositoryInterface
*/ */
public function getByIds(array $categoryIds): Collection; public function getByIds(array $categoryIds): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* Returns a list of all the categories belonging to a user. * Returns a list of all the categories belonging to a user.
* *
@@ -128,6 +128,8 @@ interface CategoryRepositoryInterface
*/ */
public function getCategories(): Collection; public function getCategories(): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* Return most recent transaction(journal) date or null when never used before. * Return most recent transaction(journal) date or null when never used before.
* *
@@ -138,8 +140,6 @@ interface CategoryRepositoryInterface
*/ */
public function lastUseDate(Category $category, Collection $accounts): ?Carbon; public function lastUseDate(Category $category, Collection $accounts): ?Carbon;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Collection $categories * @param Collection $categories
* @param Collection $accounts * @param Collection $accounts
@@ -150,6 +150,8 @@ interface CategoryRepositoryInterface
*/ */
public function periodExpenses(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array; public function periodExpenses(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Collection $accounts * @param Collection $accounts
* @param Carbon $start * @param Carbon $start
@@ -169,8 +171,6 @@ interface CategoryRepositoryInterface
*/ */
public function periodIncome(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array; public function periodIncome(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Collection $accounts * @param Collection $accounts
* @param Carbon $start * @param Carbon $start
@@ -182,6 +182,15 @@ interface CategoryRepositoryInterface
/** @noinspection MoreThanThreeArgumentsInspection */ /** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param string $query
*
* @return Collection
*/
public function searchCategory(string $query): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -26,6 +26,11 @@ use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\DoubleTransactionFilter; use FireflyIII\Helpers\Filter\DoubleTransactionFilter;
use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -36,12 +41,22 @@ use Log;
*/ */
class Search implements SearchInterface class Search implements SearchInterface
{ {
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var BillRepositoryInterface */
private $billRepository;
/** @var BudgetRepositoryInterface */
private $budgetRepository;
/** @var CategoryRepositoryInterface */
private $categoryRepository;
/** @var int */ /** @var int */
private $limit = 100; private $limit = 100;
/** @var Collection */ /** @var Collection */
private $modifiers; private $modifiers;
/** @var string */ /** @var string */
private $originalQuery = ''; private $originalQuery = '';
/** @var float */
private $startTime;
/** @var User */ /** @var User */
private $user; private $user;
/** @var array */ /** @var array */
@@ -54,8 +69,13 @@ class Search implements SearchInterface
*/ */
public function __construct() public function __construct()
{ {
$this->modifiers = new Collection; $this->modifiers = new Collection;
$this->validModifiers = (array)config('firefly.search_modifiers'); $this->validModifiers = (array)config('firefly.search_modifiers');
$this->startTime = microtime(true);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->categoryRepository = app(CategoryRepositoryInterface::class);
$this->budgetRepository = app(BudgetRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
if ('testing' === config('app.env')) { if ('testing' === config('app.env')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this)));
@@ -112,6 +132,14 @@ class Search implements SearchInterface
} }
} }
/**
* @return float
*/
public function searchTime(): float
{
return microtime(true) - $this->startTime;
}
/** /**
* @return LengthAwarePaginator * @return LengthAwarePaginator
*/ */
@@ -128,8 +156,6 @@ class Search implements SearchInterface
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
} }
$collector->setSearchWords($this->words); $collector->setSearchWords($this->words);
$collector->removeFilter(InternalTransferFilter::class); $collector->removeFilter(InternalTransferFilter::class);
$collector->addFilter(DoubleTransactionFilter::class); $collector->addFilter(DoubleTransactionFilter::class);
@@ -155,6 +181,10 @@ class Search implements SearchInterface
public function setUser(User $user): void public function setUser(User $user): void
{ {
$this->user = $user; $this->user = $user;
$this->accountRepository->setUser($user);
$this->billRepository->setUser($user);
$this->categoryRepository->setUser($user);
$this->budgetRepository->setUser($user);
} }
/** /**
@@ -167,8 +197,6 @@ class Search implements SearchInterface
{ {
/* /*
* TODO: * TODO:
* 'source', 'destination',
* 'category','budget',
* 'bill', * 'bill',
*/ */
@@ -176,6 +204,40 @@ class Search implements SearchInterface
switch ($modifier['type']) { switch ($modifier['type']) {
default: default:
die(sprintf('unsupported modifier: "%s"', $modifier['type'])); die(sprintf('unsupported modifier: "%s"', $modifier['type']));
case 'source':
// source can only be asset, liability or revenue account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE];
$accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes);
if ($accounts->count() > 0) {
$collector->setAccounts($accounts);
}
break;
case 'destination':
// source can only be asset, liability or expense account:
$searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE];
$accounts = $this->accountRepository->searchAccount($modifier['value'], $searchTypes);
if ($accounts->count() > 0) {
$collector->setOpposingAccounts($accounts);
}
break;
case 'category':
$result = $this->categoryRepository->searchCategory($modifier['value']);
if ($result->count() > 0) {
$collector->setCategories($result);
}
break;
case 'bill':
$result = $this->billRepository->searchBill($modifier['value']);
if ($result->count() > 0) {
$collector->setBills($result);
}
break;
case 'budget':
$result = $this->budgetRepository->searchBudget($modifier['value']);
if ($result->count() > 0) {
$collector->setBudgets($result);
}
break;
case 'amount_is': case 'amount_is':
case 'amount': case 'amount':
$amount = app('steam')->positive((string)$modifier['value']); $amount = app('steam')->positive((string)$modifier['value']);

View File

@@ -31,6 +31,11 @@ use Illuminate\Support\Collection;
*/ */
interface SearchInterface interface SearchInterface
{ {
/**
* @return Collection
*/
public function getModifiers(): Collection;
/** /**
* @return string * @return string
*/ */
@@ -46,6 +51,11 @@ interface SearchInterface
*/ */
public function parseQuery(string $query); public function parseQuery(string $query);
/**
* @return float
*/
public function searchTime(): float;
/** /**
* @return LengthAwarePaginator * @return LengthAwarePaginator
*/ */

View File

@@ -39,8 +39,6 @@ function searchFailure() {
function presentSearchResults(data) { function presentSearchResults(data) {
$('.search_ongoing').hide(); $('.search_ongoing').hide();
$('p.search_count').show();
$('span.search_count').text(data.count);
$('.search_box').find('.overlay').remove(); $('.search_box').find('.overlay').remove();
$('.search_results').html(data.html).show(); $('.search_results').html(data.html).show();

View File

@@ -217,7 +217,27 @@ return [
// search // search
'search' => 'Search', 'search' => 'Search',
'search_query' => 'Query', 'search_query' => 'Query',
'search_found_transactions' => 'Number of transactions found:', 'search_found_transactions' => 'Firefly III found :count transaction(s) in :time seconds.',
'search_for_query' => 'Firefly III is searching for transactions with all of these words in them: <span class="text-info">:query</span>',
'search_modifier_amount_is' => 'Amount is exactly :value',
'search_modifier_amount' => 'Amount is exactly :value',
'search_modifier_amount_max' => 'Amount is at most :value',
'search_modifier_amount_min' => 'Amount is at least :value',
'search_modifier_amount_less' => 'Amount is less than :value',
'search_modifier_amount_more' => 'Amount is more than :value',
'search_modifier_source' => 'Source account is :value',
'search_modifier_destination' => 'Destination account is :value',
'search_modifier_category' => 'Category is :value',
'search_modifier_budget' => 'Budget is :value',
'search_modifier_bill' => 'Bill is :value',
'search_modifier_type' => 'Transaction type is :value',
'search_modifier_date' => 'Transaction date is :value',
'search_modifier_date_before' => 'Transaction date is before :value',
'search_modifier_date_after' => 'Transaction date is after :value',
'search_modifier_on' => 'Transaction date is :value',
'search_modifier_before' => 'Transaction date is before :value',
'search_modifier_after' => 'Transaction date is after :value',
'modifiers_applies_are' => 'The following modifiers are applied to the search as well:',
'general_search_error' => 'An error occured while searching. Please check the log files for more information.', 'general_search_error' => 'An error occured while searching. Please check the log files for more information.',
'search_box' => 'Search', 'search_box' => 'Search',
'search_box_intro' => 'Welcome to the search function of Firefly III. Enter your search query in the box. Make sure you check out the help file because the search is pretty advanced.', 'search_box_intro' => 'Welcome to the search function of Firefly III. Enter your search query in the box. Make sure you check out the help file because the search is pretty advanced.',

View File

@@ -20,7 +20,8 @@
<div class="form-group"> <div class="form-group">
<label for="query" class="col-sm-1 control-label">{{ 'search_query'|_ }}</label> <label for="query" class="col-sm-1 control-label">{{ 'search_query'|_ }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input autocomplete="off" type="text" name="q" id="query" value="{{ fullQuery }}" class="form-control" placeholder="{{ fullQuery }}"> <input autocomplete="off" type="text" name="q" id="query" value="{{ fullQuery }}" class="form-control"
placeholder="{{ fullQuery }}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -29,6 +30,17 @@
</div> </div>
</div> </div>
</form> </form>
<p>
{{ trans('firefly.search_for_query', {query: query})|raw}}
</p>
{% if modifiers|length > 0 %}
<p>{{ trans('firefly.modifiers_applies_are') }}</p>
<ul>
{% for modifier in modifiers %}
<li>{{ trans('firefly.search_modifier_'~modifier.type, {value: modifier.value}) }}</li>
{% endfor %}
</ul>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -44,11 +56,7 @@
<p class="search_ongoing text-center" style="margin-top:70px;"> <p class="search_ongoing text-center" style="margin-top:70px;">
{{ 'search_searching'|_ }} {{ 'search_searching'|_ }}
</p> </p>
<p class="search_count" style="display: none;"> <div class="search_results" style="display: none;"></div>
{{ 'search_found_transactions'|_ }} <span class="search_count"></span>
</p>
<div class="search_results" style="display: none;">
</div>
{# loading indicator #} {# loading indicator #}
<div class="overlay"> <div class="overlay">
<i class="fa fa-refresh fa-spin"></i> <i class="fa fa-refresh fa-spin"></i>

View File

@@ -1,3 +1,8 @@
<p>
<p class="search_count">
{{ trans('firefly.search_found_transactions', {count: transactions.count, time: searchTime}) }}
</p>
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<thead> <thead>
<tr class="ignore"> <tr class="ignore">

View File

@@ -24,6 +24,7 @@ namespace Tests\Feature\Controllers;
use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Search\SearchInterface; use FireflyIII\Support\Search\SearchInterface;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
use Mockery; use Mockery;
@@ -61,6 +62,7 @@ class SearchControllerTest extends TestCase
$search->shouldReceive('parseQuery')->once(); $search->shouldReceive('parseQuery')->once();
$search->shouldReceive('getWordsAsString')->once()->andReturn('test'); $search->shouldReceive('getWordsAsString')->once()->andReturn('test');
$search->shouldReceive('getModifiers')->once()->andReturn(new Collection);
$this->be($this->user()); $this->be($this->user());
$response = $this->get(route('search.index') . '?q=test'); $response = $this->get(route('search.index') . '?q=test');
$response->assertStatus(200); $response->assertStatus(200);
@@ -78,7 +80,8 @@ class SearchControllerTest extends TestCase
$search->shouldReceive('parseQuery')->once(); $search->shouldReceive('parseQuery')->once();
$search->shouldReceive('setLimit')->withArgs([50])->once(); $search->shouldReceive('setLimit')->withArgs([50])->once();
$search->shouldReceive('searchTransactions')->once()->andReturn(new Collection); $search->shouldReceive('searchTransactions')->once()->andReturn(new LengthAwarePaginator([],0,10));
$search->shouldReceive('searchTime')->once()->andReturn(0.2);
$this->be($this->user()); $this->be($this->user());