PDA

View Full Version : [C# - LinQ]Eseguire ricerca su stringhe


RaouL_BennetH
13-03-2012, 20:09
Ciao a tutti.

gli strumenti che ho a disposizione sono:
sql server 2008, C# ed entity framework.

Devo effettuare una ricerca su parole o porzioni di frasi, in questo modo:



var result = mioOggetto.Where(c => c.Meaning.Contains(parola));



Come potete notare, usando il metodo Contains() trovo appunto qualsiasi cosa che contenga "parola".

Ciò che però non riesco a fare è passargli qualcosa di più complesso, ad esempio:

supponendo che devo trovare tutto ciò che contiene "mario pippo paperopoli"
o che almeno contenga uno di questi termini, quel metodo non mi è più sufficiente.

per essere più chiaro:


//ho dei record che contengono ad esempio

"TESTO1" "mario era andato da gigi per prendere un caffè"
"TESTO2" "pippo abita a paperopoli che è una graziosa cittadina"
"TESTO3" "mario andò con gigi a paperopoli dove fanno un ottimo caffè"



Avrei bisogno di poter fare una ricerca non solo con "mario" ma:


cerca: mario, gigi, cittadina, caffè


e dovrebbe restituirmi (in questo caso) tutti e tre i testi

o anche

cerca: mario andato caffè


e dovrebbe restituirmi solo il primo e il secondo.

Grazie a tutti :)

RaouL.

nico159
13-03-2012, 20:48
Non c'è bisogno di alcun codice personalizzato
var result = mioOggetto.Where(c => c.Meaning.Contains(parola) || c.Meaning.Contains(parola1) || c.Meaning.Contains(parola2)));

RaouL_BennetH
13-03-2012, 20:57
Non c'è bisogno di alcun codice personalizzato
var result = mioOggetto.Where(c => c.Meaning.Contains(parola) || c.Meaning.Contains(parola1) || c.Meaning.Contains(parola2)));

Ciao e grazie per il tuo suggerimento :)

Però.... io a monte non so l'utente quante parole digiterà nel campo di ricerca, quindi 'credo' che devo fare qualche passaggio in più.

RaouL_BennetH
13-03-2012, 21:52
Ho un primo prototipo ma è molto lento:



public BindingSource GetByMeanings(string meaning)
{
context = new EntityContext();
string[] strArray = meaning.Split(',');
BindingSource bs = new BindingSource();
foreach(string str in strArray)
{
var bList = context.DBOMeanings.Where(c => c.Contains(str));
localReportSource.DataSource = bList;
}

return localReportSource;
}


Ovviamente.... il 'var' nel foreach, essendo una collezione, mi fa orrore, però da qualche parte devo cominciare...

nico159
13-03-2012, 21:57
Ciao e grazie per il tuo suggerimento :)

Però.... io a monte non so l'utente quante parole digiterà nel campo di ricerca, quindi 'credo' che devo fare qualche passaggio in più.
In questo caso:

var result = mioOggetto.Where(c => listaParole.Any(c.Contains));

Per capire meglio listaParole.Any(c.Contains)
http://www.ginktage.com/2011/03/using-implicit-method-group-conversion-in-c/

RaouL_BennetH
13-03-2012, 22:16
In questo caso:

var result = mioOggetto.Where(c => listaParole.Any(c.Contains));

Per capire meglio listaParole.Any(c.Contains)
http://www.ginktage.com/2011/03/using-implicit-method-group-conversion-in-c/

Hai fatto bene a sottolineare: "per capire meglio listaParole.Any(c.Contains)"

:D

sto dando un occhio a quel link ma non mi è chiaro nulla, cioè:

il mio oggetto è un'entità mappata su una tabella del db;

"listaParole", è l'insieme di stringhe che prendo da una casella di testo, con il metodo Split(), per es.: "abc,caffè,barca"

Qualsiasi cosa cerco di fare per "capire" l'esempio, non mi fa neanche compilare...

nico159
13-03-2012, 22:34
Quella non è altra che una forma abbreviata per scrivere:

var result = mioOggetto.Where(c => listaParole.Any(parola => c.Contains(parola)));

E' strano che non ti fa compilare, controlla di che tipo è c

Che errore ti da il compilatore?

RaouL_BennetH
13-03-2012, 22:52
Quella non è altra che una forma abbreviata per scrivere:

var result = mioOggetto.Where(c => listaParole.Any(parola => c.Contains(parola)));

E' strano che non ti fa compilare, controlla di che tipo è c

Che errore ti da il compilatore?

Beh... "c" dev'essere per forza di tipo "mioOggetto"

nico159
13-03-2012, 23:01
Beh... "c" dev'essere per forza di tipo "mioOggetto"
c dev'essere una stringa se vuoi che il codice scritto prima funzioni e mioOggetto una collezione di stringhe

RaouL_BennetH
13-03-2012, 23:13
c dev'essere una stringa se vuoi che il codice scritto prima funzioni e mioOggetto una collezione di stringhe

ecco svelato l'arcano allora :)

io sto facendo una ricerca su un oggetto che altro non è che la rappresentazione di una tabella nel db (mediante entity framework).

Ho notato comunque che qualcosa non mi torna se uso banalmente linq to sql:



string[] listaParole = myText.Split(',');

var list = from c in contex.DBOMeanings
where listaParole.Contains(c.Meanings)
select c;



Che dovrebbe appunto tradurla in un where meaning = "blah" OR meaning = "BU";

... beh... non trova nulla....

RaouL_BennetH
14-03-2012, 10:51
al momento ho risolto un primo problema così:


string[] listaParole = sometext.Split(,);
var query = context.DBOMeanings.ToList();
List<DBOMeanings> results = new List<DBOMeanings>();

foreach(var obj in query)
{
foreach(string s in listaParole)
{
if(obj.meaning.Contains(s))
{
results.Add(obj);
}
}
}


in questo modo ottengo i risultati che mi aspetto....
....tranne che per l'ordinamento :muro:

dovrei ordinarli in modo che:
cerco "piano,pavimento"

se un record le contiene entrambe dev'essere il primo della lista
e poi man mano quelli che ne contengono solo una...

gugoXX
14-03-2012, 12:09
Ma e LinQ ?


static void Main(string[] args)
{
const string sometext = "primo, secondo";
var tosearch = new[]
{
new {meaning = "quello che fa il primo", altroacaso = 2},
new {meaning = "non lo fa il secondo", altroacaso = 3},
new {meaning = "e il terzo non vedra'", altroacaso = 1},
new {meaning = "quello che il secondo e il primo faranno", altroacaso = 2},
new {meaning = "il quarto dorme", altroacaso = 2}
};


string[] listaParole = sometext.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

// Primo risultato, complessita' O(N^2)
var results = from row in tosearch
from parola in listaParole
where row.meaning.Contains(parola)
let res = new {row, parola}
group res by row into grp
orderby grp.Count() descending
select grp.Key;

results.ToList().ForEach(Console.WriteLine);
Console.WriteLine("-------------");
Console.WriteLine();
Console.ReadKey();

// Secondo risultato, complessita' inferiore a O(N^2), probabilmente O(N)

// Vado di Regex, costruisco il pattern da cercare a partire dalla comma separated string di input
// Esempio: "(primo|secondo)"

string[] listaParoleNuova = sometext.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
.Select(word => word.Trim())
.ToArray();

string joinedWithBar = string.Join("|", listaParoleNuova);
string regexPattern = string.Format("({0})", joinedWithBar);

var regex = new Regex(regexPattern, RegexOptions.Compiled);

var results2 = from row in tosearch
let matches = regex.Matches(row.meaning)
let count = matches.Count
where count > 0
orderby count descending
select row;

results2.ToList().ForEach(Console.WriteLine);
Console.WriteLine("-------------");
Console.ReadKey();
}

RaouL_BennetH
14-03-2012, 23:40
Ciao gugoXX :)

Ho solo un piccolo problema nell' "importare" la tua spiegazione alla mia problematica.

Usando LinQ to Entity ottengo che:


LINQ to Entities does not recognize the method 'System.Text.RegularExpressions.MatchCollection Matches(System.String)' method, and this method cannot be translated into a store expression.


Sto già vedendo se c'è un'alternativa.

gugoXX
15-03-2012, 22:47
Prova a commentare da
Secondo Risulato

in poi.

U235
16-03-2012, 14:51
al momento ho risolto un primo problema così:


string[] listaParole = sometext.Split(,);
var query = context.DBOMeanings.ToList();
List<DBOMeanings> results = new List<DBOMeanings>();

foreach(var obj in query)
{
foreach(string s in listaParole)
{
if(obj.meaning.Contains(s))
{
results.Add(obj);
}
}
}


in questo modo ottengo i risultati che mi aspetto....
....tranne che per l'ordinamento :muro:

dovrei ordinarli in modo che:
cerco "piano,pavimento"

se un record le contiene entrambe dev'essere il primo della lista
e poi man mano quelli che ne contengono solo una...

Ciao, credo che la differenza sia proprio su quel "context.DBOMeanings.ToList();", nel senso che se stai usando EF e non richiami in qualche modo il caricamento di quella tabella con appunto un ToList() o un ToArray() o un semplice Load(), la tabella non verrà valorizzata.

quindi se è come sto pensando, nella tua soluzione Linq dei aggiungere uno di quei metodi, quindi una cosa del genere :


string[] listaParole = myText.Split(',');

var list = from c in contex.DBOMeanings.ToList()
where listaParole.Contains(c.Meanings)
select c;

RaouL_BennetH
17-03-2012, 23:26
Prova a commentare da
Secondo Risulato

in poi.

Ok, senza usare le regex il tuo metodo funziona. Certo ho da lavorarci su
perchè ho qualche risultato che non mi aspetto, ma mi hai già praticamente fornito tutte le informazioni necessarie per risolverlo.


@U235

E' già tutto caricato, non ho incollato solo il pezzo di codice che riguarda
il load.

U235
18-03-2012, 00:04
Ciao,
una cosa del genere ?


List<MiaTabella> MiaTabella = new List<MiaTabella>();
MiaTabella.Add(new MiaTabella() { AltroCampo = "123456", Messaggio = "mario era andato da gigi per prendere un caffè" });
MiaTabella.Add(new MiaTabella() { AltroCampo = "123456", Messaggio = "pippo abita a paperopoli che è una graziosa cittadina" });
MiaTabella.Add(new MiaTabella() { AltroCampo = "123456", Messaggio = "mario andò con gigi a paperopoli dove fanno un ottimo caffè" });
List<string> Parole = new List<string>();
Parole = new List<string>();
Parole.Add("mario");
Parole.Add("gigi");
//Parole.Add("cittadina");//se aggiungi questa ti trova anche la seconda
Parole.Add("caffè");
MiaTabella = MiaTabella.Where(w => w.Messaggio.Split(' ').Any(p => Parole.Select(c => c.ToUpper()).Contains(p.ToUpper()))).ToList();

U235
18-03-2012, 00:43
se poi volessi ad esempio avere in un altro campo le parole che sono state trovarte nella frase, allora potresti fare una cosa del genere :

List<MiaTabella> MiaTabella = new List<MiaTabella>();
MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "mario era andato da gigi per prendere un caffè" });
MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "pippo abita a paperopoli che è una graziosa cittadina" });
MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "mario andò con gigi a paperopoli dove fanno un ottimo caffè" });
List<string> Parole = new List<string>();
Parole = new List<string>();
Parole.Add("mario");
Parole.Add("gigi");
Parole.Add("cittadina");//se aggiungi questa ti trova anche la seconda
Parole.Add("caffè");
MiaTabella = MiaTabella.Where(
w => w.Messaggio.Split(' ')
.Where(p => Parole.Select(c => c.ToUpper()).Contains(p.ToUpper()))
.Select(sw => w.Parole.AddString(sw)).ToList().Any()).ToList();


ma con un piccolo accorgimento :


public class MiaTabella
{
public string Messaggio { get; set; }
public MiaListaString Parole { get; set; }
}

public class MiaListaString : List<string>
{
public bool AddString(string item)
{
base.Add(item);
return true;
}
}


ovvero far restituire un valore bool dal metodo AddString (MiaListaString), visto che Add è un void.

il risultato che avresti sarebbe :

MiaTabella[0].Messaggio = "mario era andato da gigi per prendere un caffè"
MiaTabella[0].Parole = [0] = "mario" [1] = "gigi" [2] = "caffè"

//questo solo se c'è tra le parole anche "cittadina"
MiaTabella[1].Messaggio = "pippo abita a paperopoli che è una graziosa cittadina"
MiaTabella[1].Parole = [0] = "cittadina"

MiaTabella[2].Messaggio = "mario andò con gigi a paperopoli dove fanno un ottimo caffè"
MiaTabella[2].Parole = [0] = "mario" [1] = "gigi" [2] = "caffè"

ma almeno avevo capito bene ciò che ti serviva? :stordita:




EDIT :
non avevo letto dell'ordinamento :fagiano:

MiaTabella = MiaTabella.Where(
w => w.Messaggio.Split(' ')
.Where(p => Parole.Select(c => c.ToUpper()).Contains(p.ToUpper()))
.Select(sw => w.Parole.AddString(sw)).ToList().Any())
.OrderByDescending(o=>o.Parole.Count).ToList();


ora il risultato da :

MiaTabella[0].Messaggio = "mario era andato da gigi per prendere un caffè"
MiaTabella[0].Parole = [0] = "mario" [1] = "gigi" [2] = "caffè"

MiaTabella[1].Messaggio = "mario andò con gigi a paperopoli dove fanno un ottimo caffè"
MiaTabella[1].Parole = [0] = "mario" [1] = "gigi" [2] = "caffè"

//questo solo se c'è tra le parole anche "cittadina"
MiaTabella[2].Messaggio = "pippo abita a paperopoli che è una graziosa cittadina"
MiaTabella[2].Parole = [0] = "cittadina"

U235
18-03-2012, 14:32
se poi volessi ordinare per numero di occorrenze DIFFERENTI tra loro in primo luogo e in secondo luogo numero di occorrenze in generale (anche doppioni, allora potresti fare questo :

MiaTabella = MiaTabella.Where(
w => w.Messaggio.Split()
.Where(p => Parole.Select(c => c.ToUpper()).Contains(p.ToUpper()))
.Select(sw => w.Parole.AddString(sw)).ToList().Any())
.OrderByDescending(o=>o.Parole.Count)
.OrderByDescending(o2 => o2.Parole.Distinct().Count())
.ToList();


in questo caso se abbiamo queste stringe :

MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "cittadina mario era andato da gigi per prendere un caffè" });
MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "gigi pippo abita a paperopoli che è una graziosa cittadina" });
MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "gigi gigi gigi gigi gigi pippo abita a paperopoli che è una graziosa cittadina" });
MiaTabella.Add(new MiaTabella() { Parole = new MiaListaString(), Messaggio = "gigi mario andò con gigi a paperopoli dove fanno un ottimo caffè" });


il contenuto di MiaTabella sarà :

MiaTabella[0].Messaggio = "cittadina mario era andato da gigi per prendere un caffè"
MiaTabella[0].Parole = [0] = "cittadina" [1] = "mario" [2] = "gigi" [3] = "caffè"

MiaTabella[1].Messaggio = "gigi mario andò con gigi a paperopoli dove fanno un ottimo caffè"
MiaTabella[1].Parole = [0] = "gigi" [1] = "mario" [2] = "gigi" [3] = "caffè"

MiaTabella[2].Messaggio = "gigi gigi gigi gigi gigi pippo abita a paperopoli che è una graziosa cittadina"
MiaTabella[2].Parole = [0] = "gigi" [1] = "gigi" [2] = "gigi" [3] = "gigi" [4] = "gigi" [5] = "cittadina"

MiaTabella[3].Messaggio = "pippo abita a paperopoli che è una graziosa cittadina"
MiaTabella[3].Parole = [0] = "gigi" [1] = "cittadina"

ciao.

P.S.
mi hai dato una "spinta" per fare una cosa che mi serviva, spero ti sia utile anche a te :)

RaouL_BennetH
21-03-2012, 11:42
@U235:

Ciao e grazie anche a te per il prezioso aiuto ! :)


La tua idea funziona bene se resto nei limiti di circa 10.000 record.

Passando oltre, non ho ancora capito il perchè, mi succede questo:

1) Mi trova corrispondenze che non sono nei termini di ricerca
2) La ricerca diventa molto lenta, considerando che agisce su una tabella contenente poco meno di un milione di records.

Oggi devo fare altri test e posso riportare qualcosa di più preciso.

RaouL.

U235
21-03-2012, 23:23
@U235:

Ciao e grazie anche a te per il prezioso aiuto ! :)


La tua idea funziona bene se resto nei limiti di circa 10.000 record.

Passando oltre, non ho ancora capito il perchè, mi succede questo:

1) Mi trova corrispondenze che non sono nei termini di ricerca
2) La ricerca diventa molto lenta, considerando che agisce su una tabella contenente poco meno di un milione di records.

Oggi devo fare altri test e posso riportare qualcosa di più preciso.

RaouL.

Ciao, io nei test che ho utilizzato 1000004 oggetti e non ho riscontrato il primo problema che hai menzionato, ovviamente parliamo di stringhe lunghe come quelle sopra e chiavi di ricerca uguali (4). poi bisogna vedere se intendi case sensitive, io ho fatto un case insensitive, ma basta togliere i ToUpper().
per quanto riguarda la velocità è relativo, ovviamente una ricerca del genere richiede tempo, poi non so se si possa ottimizzare (ho fatto dei test parallelizzando e ho un leggero miglioramento delle prestazioni di circa 10% nel mio pc), comunque se cerchi la velocità allora non credo che linq sia la strada giusta.

comunque, per curiosità, com quale algoritmo lo hai confrontato? svolge le stesse funzioni? è sempre linq? se è più veloce interessa anche a me :)

RaouL_BennetH
27-03-2012, 15:10
Ciao, io nei test che ho utilizzato 1000004 oggetti e non ho riscontrato il primo problema che hai menzionato, ovviamente parliamo di stringhe lunghe come quelle sopra e chiavi di ricerca uguali (4). poi bisogna vedere se intendi case sensitive, io ho fatto un case insensitive, ma basta togliere i ToUpper().
per quanto riguarda la velocità è relativo, ovviamente una ricerca del genere richiede tempo, poi non so se si possa ottimizzare (ho fatto dei test parallelizzando e ho un leggero miglioramento delle prestazioni di circa 10% nel mio pc), comunque se cerchi la velocità allora non credo che linq sia la strada giusta.

comunque, per curiosità, com quale algoritmo lo hai confrontato? svolge le stesse funzioni? è sempre linq? se è più veloce interessa anche a me :)

Ciao :)

Non sono ancora arrivato a trovare una soluzione ottimale al problema.

Al momento, usando linq "puro", seguendo i suggerimenti e le idee di gugoXX e le tue vado un pò più veloce.
Credo comunque di intuire che possa dipendere anche dai tipi di dati che vengono passati al programma, riferendomi al formato e al contenuto del file che viene di volta in volta passato.